Create session manager for workers to use to interact with sessions (#2235)

This commit is contained in:
Todd authored and GitHub committed 2022-07-06 13:37:49 -07:00
1 parent 6cbf598369
commit 155c5f578b
19 files changed
+1459 -673

No files matched your search

+2 -3
View File
@@ -110,9 +110,8 @@ func TestRotationTicking(t *testing.T) {
// are valid; if not, we won't establish a new connection and rotation
// will fail
w.Worker().Resolver().UpdateState(resolver.State{Addresses: []resolver.Address{}})
cc, err := w.Worker().GrpcClientConn()
require.NoError(err)
require.NoError(cc.Close())
require.NotNil(w.Worker().GrpcClientConn)
require.NoError(w.Worker().GrpcClientConn.Close())
require.NoError(w.Worker().StartControllerConnections())
}
}
@@ -197,10 +197,9 @@ func (w *Worker) createClientConn(addr string) error {
if err != nil {
return fmt.Errorf("error dialing controller for worker auth: %w", err)
}
w.grpcClientConn.Store(cc)
w.GrpcClientConn = cc
w.controllerStatusConn.Store(pbs.NewServerCoordinationServiceClient(cc))
w.controllerSessionConn.Store(pbs.NewSessionServiceClient(cc))
w.controllerMultihopConn.Store(multihop.NewMultihopServiceClient(cc))
return nil
}
+25 -51
View File
@@ -20,6 +20,7 @@ import (
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/boundary/internal/proxy"
"github.com/hashicorp/go-secure-stdlib/listenerutil"
"google.golang.org/protobuf/types/known/timestamppb"
"nhooyr.io/websocket"
"nhooyr.io/websocket/wspb"
)
@@ -30,12 +31,12 @@ type HandlerProperties struct {
// Handler returns a http.Handler for the API. This can be used on
// its own to mount the Worker API within another web server.
func (w *Worker) handler(props HandlerProperties) (http.Handler, error) {
func (w *Worker) handler(props HandlerProperties, sm session.Manager) (http.Handler, error) {
const op = "worker.(Worker).handler"
// Create the muxer to handle the actual endpoints
mux := http.NewServeMux()
h, err := w.handleProxy(props.ListenerConfig)
h, err := w.handleProxy(props.ListenerConfig, sm)
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
@@ -46,7 +47,7 @@ func (w *Worker) handler(props HandlerProperties) (http.Handler, error) {
return metricHandler, nil
}
func (w *Worker) handleProxy(listenerCfg *listenerutil.ListenerConfig) (http.HandlerFunc, error) {
func (w *Worker) handleProxy(listenerCfg *listenerutil.ListenerConfig, sessionManager session.Manager) (http.HandlerFunc, error) {
const op = "worker.(Worker).handleProxy"
if listenerCfg == nil {
return nil, fmt.Errorf("%s: missing listener config", op)
@@ -98,21 +99,12 @@ func (w *Worker) handleProxy(listenerCfg *listenerutil.ListenerConfig) (http.Han
wr.WriteHeader(http.StatusInternalServerError)
}
siRaw, valid := w.sessionInfoMap.Load(sessionId)
if !valid {
event.WriteError(ctx, op, errors.New("session not found in info map"), event.WithInfo("session_id", sessionId))
sess := sessionManager.Get(sessionId)
if sess == nil {
event.WriteError(ctx, op, errors.New("session not found locally"), event.WithInfo("session_id", sessionId))
wr.WriteHeader(http.StatusInternalServerError)
return
}
si := siRaw.(*session.Info)
si.RLock()
expiration := si.LookupSessionResponse.GetExpiration()
tofuToken := si.LookupSessionResponse.GetTofuToken()
version := si.LookupSessionResponse.GetVersion()
endpoint := si.LookupSessionResponse.GetEndpoint()
credentials := si.LookupSessionResponse.GetCredentials()
sessStatus := si.Status
si.RUnlock()
opts := &websocket.AcceptOptions{
Subprotocols: []string{globals.TcpProxyV1},
@@ -126,18 +118,9 @@ func (w *Worker) handleProxy(listenerCfg *listenerutil.ListenerConfig) (http.Han
// Later calls will cause this to noop if they return a different status
defer conn.Close(websocket.StatusNormalClosure, "done")
connCtx, connCancel := context.WithDeadline(ctx, expiration.AsTime())
connCtx, connCancel := context.WithDeadline(ctx, sess.GetExpiration())
defer connCancel()
sessClient, err := w.ControllerSessionConn()
if err != nil {
event.WriteError(ctx, op, err)
if err = conn.Close(websocket.StatusInternalError, "unable to get controller session client"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
return
}
var handshake proxy.ClientHandshake
if err := wspb.Read(connCtx, conn, &handshake); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error reading handshake from client"))
@@ -155,8 +138,7 @@ func (w *Worker) handleProxy(listenerCfg *listenerutil.ListenerConfig) (http.Han
}
if handshake.Command == proxy.HANDSHAKECOMMAND_HANDSHAKECOMMAND_SESSION_CANCEL {
_, err := session.Cancel(ctx, sessClient, sessionId)
if err != nil {
if err := sess.RequestCancel(ctx); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("unable to cancel session"))
if err = conn.Close(websocket.StatusInternalError, "unable to cancel session"); err != nil && !errors.Is(err, io.EOF) {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
@@ -169,8 +151,8 @@ func (w *Worker) handleProxy(listenerCfg *listenerutil.ListenerConfig) (http.Han
return
}
if tofuToken != "" {
if tofuToken != handshake.GetTofuToken() {
if sess.GetTofuToken() != "" {
if sess.GetTofuToken() != handshake.GetTofuToken() {
event.WriteError(ctx, op, errors.New("WARNING: mismatched tofu token"), event.WithInfo("session_id", sessionId))
if err = conn.Close(websocket.StatusPolicyViolation, "tofu token not allowed"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
@@ -178,7 +160,7 @@ func (w *Worker) handleProxy(listenerCfg *listenerutil.ListenerConfig) (http.Han
return
}
} else {
if sessStatus != pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING {
if sess.GetStatus() != pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING {
event.WriteError(ctx, op, errors.New("no tofu token but not in correct session state"), event.WithInfo("session_id", sessionId))
if err = conn.Close(websocket.StatusInternalError, "refusing to activate session"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
@@ -186,7 +168,7 @@ func (w *Worker) handleProxy(listenerCfg *listenerutil.ListenerConfig) (http.Han
return
}
if handshake.Command == proxy.HANDSHAKECOMMAND_HANDSHAKECOMMAND_UNSPECIFIED {
sessStatus, err = session.Activate(ctx, sessClient, sessionId, handshake.GetTofuToken(), version)
err = sess.RequestActivate(ctx, handshake.GetTofuToken())
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("unable to validate session"))
if err = conn.Close(websocket.StatusInternalError, "unable to activate session"); err != nil {
@@ -198,10 +180,10 @@ func (w *Worker) handleProxy(listenerCfg *listenerutil.ListenerConfig) (http.Han
}
}
// Verify the protocol has a supported proxy before calling AuthorizeConnection
endpointUrl, err := url.Parse(endpoint)
// Verify the protocol has a supported proxy before calling RequestAuthorizeConnection
endpointUrl, err := url.Parse(sess.GetEndpoint())
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("worker failed to parse target endpoint", "endpoint", endpoint))
event.WriteError(ctx, op, err, event.WithInfoMsg("worker failed to parse target endpoint", "endpoint", sess.GetEndpoint()))
if err = conn.Close(websocket.StatusProtocolError, "unsupported-protocol"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
@@ -225,9 +207,9 @@ func (w *Worker) handleProxy(listenerCfg *listenerutil.ListenerConfig) (http.Han
}
workerId := w.LastStatusSuccess().WorkerId
var ci *session.ConnInfo
var ci session.ConnInfo
var connsLeft int32
ci, connsLeft, err = session.AuthorizeConnection(ctx, sessClient, workerId, sessionId)
ci, connsLeft, err = sess.RequestAuthorizeConnection(ctx, workerId, connCancel)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("unable to authorize connection"))
if err = conn.Close(websocket.StatusInternalError, "unable to authorize connection"); err != nil {
@@ -237,22 +219,14 @@ func (w *Worker) handleProxy(listenerCfg *listenerutil.ListenerConfig) (http.Han
}
event.WriteSysEvent(ctx, op, "connection successfully authorized", "session_id", sessionId, "connection_id", ci.Id)
defer func() {
if session.CloseConnections(ctx, sessClient, w.sessionInfoMap, map[string]string{ci.Id: si.Id}) {
if sessionManager.RequestCloseConnections(ctx, map[string]string{ci.Id: sess.GetId()}) {
event.WriteSysEvent(ctx, op, "connection closed", "session_id", sessionId, "connection_id", ci.Id)
}
}()
si.Lock()
ci.ConnCtx = connCtx
ci.ConnCancel = connCancel
si.ConnInfoMap[ci.Id] = ci
si.Status = sessStatus
connectionLimit := si.LookupSessionResponse.GetConnectionLimit()
si.Unlock()
handshakeResult := &proxy.HandshakeResult{
Expiration: expiration,
ConnectionLimit: connectionLimit,
Expiration: timestamppb.New(sess.GetExpiration()),
ConnectionLimit: sess.GetConnectionLimit(),
ConnectionsLeft: connsLeft,
}
if err := wspb.Write(connCtx, conn, handshakeResult); err != nil {
@@ -267,9 +241,8 @@ func (w *Worker) handleProxy(listenerCfg *listenerutil.ListenerConfig) (http.Han
UserClientIp: net.ParseIP(userClientIp),
ClientAddress: clientAddr,
ClientConn: conn,
RemoteEndpoint: endpoint,
SessionClient: sessClient,
SessionInfo: si,
RemoteEndpoint: sess.GetEndpoint(),
Session: sess,
ConnectionId: ci.Id,
}
@@ -281,13 +254,14 @@ func (w *Worker) handleProxy(listenerCfg *listenerutil.ListenerConfig) (http.Han
return
}
credentials := sess.GetCredentials()
var proxyOpts []proxyHandlers.Option
if len(credentials) > 0 {
proxyOpts = append(proxyOpts, proxyHandlers.WithEgressCredentials(credentials))
}
if err = handleProxyFn(connCtx, conf, proxyOpts...); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error handling proxy", "session_id", sessionId, "endpoint", endpoint))
event.WriteError(ctx, op, err, event.WithInfoMsg("error handling proxy", "session_id", sessionId, "endpoint", sess.GetEndpoint()))
if err = conn.Close(websocket.StatusInternalError, "unable to establish proxy"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
+7 -7
View File
@@ -14,6 +14,7 @@ import (
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/daemon/cluster/handlers"
"github.com/hashicorp/boundary/internal/daemon/worker/session"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/go-multierror"
@@ -26,7 +27,7 @@ import (
"google.golang.org/grpc"
)
func (w *Worker) startListeners() error {
func (w *Worker) startListeners(sm session.Manager) error {
const op = "worker.(Worker).startListeners"
e := event.SysEventer()
@@ -41,7 +42,7 @@ func (w *Worker) startListeners() error {
return fmt.Errorf("%s: nil proxy listener", op)
}
workerServer, err := w.configureForWorker(w.proxyListener, logger)
workerServer, err := w.configureForWorker(w.proxyListener, logger, sm)
if err != nil {
return fmt.Errorf("%s: failed to configure for worker: %w", op, err)
}
@@ -51,10 +52,9 @@ func (w *Worker) startListeners() error {
return nil
}
func (w *Worker) configureForWorker(ln *base.ServerListener, logger *log.Logger) (func(), error) {
func (w *Worker) configureForWorker(ln *base.ServerListener, logger *log.Logger, sessionManager session.Manager) (func(), error) {
const op = "worker.configureForWorker"
handler, err := w.handler(HandlerProperties{ListenerConfig: ln.Config})
handler, err := w.handler(HandlerProperties{ListenerConfig: ln.Config}, sessionManager)
if err != nil {
return nil, err
}
@@ -126,7 +126,7 @@ func (w *Worker) configureForWorker(ln *base.ServerListener, logger *log.Logger)
Storage: w.WorkerAuthStorage,
BaseListener: ln.ProxyListener,
BaseTlsConfiguration: &tls.Config{
GetConfigForClient: w.getSessionTls,
GetConfigForClient: w.getSessionTls(sessionManager),
},
FetchCredsFunc: fetchCredsFn,
GenerateServerCertificatesFunc: generateServerCertificatesFn,
@@ -150,7 +150,7 @@ func (w *Worker) configureForWorker(ln *base.ServerListener, logger *log.Logger)
return nil, fmt.Errorf("%s: error creating multihop service handler: %w", op, err)
}
multihop.RegisterMultihopServiceServer(downstreamServer, multihopService)
statusSessionService := NewWorkerProxyServiceServer(w.controllerStatusConn, w.controllerSessionConn)
statusSessionService := NewWorkerProxyServiceServer(w.GrpcClientConn, w.controllerStatusConn)
pbs.RegisterServerCoordinationServiceServer(downstreamServer, statusSessionService)
pbs.RegisterSessionServiceServer(downstreamServer, statusSessionService)
+5 -1
View File
@@ -9,6 +9,8 @@ import (
"testing"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/daemon/worker/session"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/go-secure-stdlib/listenerutil"
"github.com/stretchr/testify/require"
)
@@ -76,7 +78,9 @@ func TestStartListeners(t *testing.T) {
require.NoError(t, err)
w.baseContext = context.Background()
err = w.startListeners()
manager, err := session.NewManager(pbs.NewSessionServiceClient(w.GrpcClientConn))
require.NoError(t, err)
err = w.startListeners(manager)
if tt.expErr {
require.EqualError(t, err, tt.expErrMsg)
return
+4 -8
View File
@@ -7,7 +7,6 @@ import (
"sync"
"github.com/hashicorp/boundary/internal/daemon/worker/session"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"nhooyr.io/websocket"
)
@@ -24,9 +23,8 @@ type Config struct {
ClientConn *websocket.Conn
RemoteEndpoint string
SessionClient pbs.SessionServiceClient
SessionInfo *session.Info
ConnectionId string
Session session.Session
ConnectionId string
}
// Validate checks that the provided config is valid. If invalid, an error is returned
@@ -39,10 +37,8 @@ func (c Config) Validate() error {
return errors.New("missing client connection")
case c.RemoteEndpoint == "":
return errors.New("missing remote endpoint")
case c.SessionClient == nil:
return errors.New("missing session client")
case c.SessionInfo == nil:
return errors.New("missing session info")
case c.Session == nil:
return errors.New("missing session")
case c.ConnectionId == "":
return errors.New("missing connection id")
default:
+12 -30
View File
@@ -6,12 +6,15 @@ import (
"testing"
"github.com/hashicorp/boundary/internal/daemon/worker/session"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"nhooyr.io/websocket"
)
type testSession struct {
session.Session
}
func TestConfigValidate(t *testing.T) {
t.Parallel()
@@ -20,10 +23,7 @@ func TestConfigValidate(t *testing.T) {
IP: net.ParseIP("127.0.0.1"),
Port: 50000,
}
sessClient := pbs.NewMockSessionServiceClient()
si := &session.Info{
Id: "one",
}
si := testSession{}
conn := &websocket.Conn{}
tests := []struct {
@@ -37,8 +37,7 @@ func TestConfigValidate(t *testing.T) {
conf: Config{
ClientConn: conn,
RemoteEndpoint: "tcp://remote",
SessionClient: sessClient,
SessionInfo: si,
Session: si,
ConnectionId: "connection-id",
},
wantErr: true,
@@ -49,8 +48,7 @@ func TestConfigValidate(t *testing.T) {
conf: Config{
ClientAddress: clientAddr,
RemoteEndpoint: "tcp://remote",
SessionClient: sessClient,
SessionInfo: si,
Session: si,
ConnectionId: "connection-id",
},
wantErr: true,
@@ -61,36 +59,22 @@ func TestConfigValidate(t *testing.T) {
conf: Config{
ClientAddress: clientAddr,
ClientConn: conn,
SessionClient: sessClient,
SessionInfo: si,
Session: si,
ConnectionId: "connection-id",
},
wantErr: true,
wantErrMsg: "missing remote endpoint",
},
{
name: "missing-session-client",
name: "missing-session",
conf: Config{
ClientAddress: clientAddr,
ClientConn: conn,
RemoteEndpoint: "tcp://remote",
SessionInfo: si,
ConnectionId: "connection-id",
},
wantErr: true,
wantErrMsg: "missing session client",
},
{
name: "missing-session-info",
conf: Config{
ClientAddress: clientAddr,
ClientConn: conn,
RemoteEndpoint: "tcp://remote",
SessionClient: sessClient,
ConnectionId: "connection-id",
},
wantErr: true,
wantErrMsg: "missing session info",
wantErrMsg: "missing session",
},
{
name: "missing-connection-id",
@@ -98,8 +82,7 @@ func TestConfigValidate(t *testing.T) {
ClientAddress: clientAddr,
ClientConn: conn,
RemoteEndpoint: "tcp://remote",
SessionClient: sessClient,
SessionInfo: si,
Session: si,
},
wantErr: true,
wantErrMsg: "missing connection id",
@@ -110,8 +93,7 @@ func TestConfigValidate(t *testing.T) {
ClientAddress: clientAddr,
ClientConn: conn,
RemoteEndpoint: "tcp://remote",
SessionClient: sessClient,
SessionInfo: si,
Session: si,
ConnectionId: "connection-id",
},
},
+1 -8
View File
@@ -9,7 +9,6 @@ import (
"sync"
"github.com/hashicorp/boundary/internal/daemon/worker/proxy"
"github.com/hashicorp/boundary/internal/daemon/worker/session"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"nhooyr.io/websocket"
)
@@ -56,16 +55,10 @@ func handleProxy(ctx context.Context, conf proxy.Config, _ ...proxy.Option) erro
UserClientIp: conf.UserClientIp.String(),
}
connStatus, err := session.ConnectConnection(ctx, conf.SessionClient, connectionInfo)
if err != nil {
if err := conf.Session.RequestConnectConnection(ctx, connectionInfo); err != nil {
return fmt.Errorf("error marking connection as connected: %w", err)
}
// Update connection info to set connection status
conf.SessionInfo.Lock()
conf.SessionInfo.ConnInfoMap[conf.ConnectionId].Status = connStatus
conf.SessionInfo.Unlock()
// Get a wrapped net.Conn so we can use io.Copy
netConn := websocket.NetConn(ctx, conn, websocket.MessageBinary)
+55 -10
View File
@@ -2,9 +2,14 @@ package tcp
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"fmt"
"math/big"
"net"
"testing"
"time"
"github.com/hashicorp/boundary/internal/daemon/worker/proxy"
"github.com/hashicorp/boundary/internal/daemon/worker/session"
@@ -13,6 +18,7 @@ import (
"github.com/hashicorp/boundary/sdk/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"nhooyr.io/websocket"
)
@@ -49,24 +55,41 @@ func TestHandleTcpProxyV1(t *testing.T) {
Port: 50000,
}
sessClient := pbs.NewMockSessionServiceClient()
si := &session.Info{
Id: "one",
LookupSessionResponse: &pbs.LookupSessionResponse{
sessClient.LookupSessionFn = func(_ context.Context, request *pbs.LookupSessionRequest) (*pbs.LookupSessionResponse, error) {
cert, _, _ := createTestCert(t)
return &pbs.LookupSessionResponse{
Authorization: &targets.SessionAuthorizationData{
SessionId: "mock-session",
SessionId: request.GetSessionId(),
Certificate: cert,
},
},
ConnInfoMap: map[string]*session.ConnInfo{
"mock-connection": {},
},
Expiration: timestamppb.New(time.Now().Add(time.Hour)),
}, nil
}
sessClient.AuthorizeConnectionFn = func(_ context.Context, req *pbs.AuthorizeConnectionRequest) (*pbs.AuthorizeConnectionResponse, error) {
return &pbs.AuthorizeConnectionResponse{
ConnectionId: "mock-connection",
Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_AUTHORIZED,
ConnectionsLeft: -1,
}, nil
}
sessClient.ConnectConnectionFn = func(_ context.Context, _ *pbs.ConnectConnectionRequest) (*pbs.ConnectConnectionResponse, error) {
return &pbs.ConnectConnectionResponse{
Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CONNECTED,
}, nil
}
manager, err := session.NewManager(sessClient)
require.NoError(err)
s, err := manager.LoadLocalSession(ctx, "one", "workerid")
require.NoError(err)
_, connCancelFn := context.WithCancel(context.Background())
_, _, err = s.RequestAuthorizeConnection(ctx, "workerid", connCancelFn)
require.NoError(err)
conf := proxy.Config{
ClientAddress: clientAddr,
ClientConn: proxyConn,
RemoteEndpoint: fmt.Sprintf("tcp://localhost:%d", port),
SessionClient: sessClient,
SessionInfo: si,
Session: s,
ConnectionId: "mock-connection",
UserClientIp: net.ParseIP("127.0.0.1"),
}
@@ -109,3 +132,25 @@ func TestHandleTcpProxyV1(t *testing.T) {
cancelCtx()
}
func createTestCert(t *testing.T) ([]byte, ed25519.PublicKey, ed25519.PrivateKey) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
template := &x509.Certificate{
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageCertSign,
SerialNumber: big.NewInt(0),
NotBefore: time.Now().Add(-30 * time.Second),
NotAfter: time.Now().Add(5 * time.Minute),
BasicConstraintsValid: true,
IsCA: true,
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
DNSNames: []string{"/tmp/boundary-opslistener-test0.sock", "/tmp/boundary-opslistener-test1.sock"},
}
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv)
require.NoError(t, err)
return certBytes, pub, priv
}
+139
View File
@@ -0,0 +1,139 @@
package session
import (
"context"
"fmt"
"reflect"
"sync"
"github.com/hashicorp/boundary/internal/errors"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
)
// Manager stores session information locally and exposes ways to operate on the
// set of sessions locally in batch.
// This is thread-safe.
type Manager interface {
Get(string) Session
// ForEachLocalSession calls the provided function with each Session object.
// If the provided function ever returns false the iteration stops.
// If changes to the sessions in the manager happen concurrently, this function
// does not guarantee that the key will or will not be provided in the
// iteration.
ForEachLocalSession(func(Session) bool)
// LoadLocalSession looks up from the source of truth the session information,
// validates it is valid, and then refreshes the local manager's data.
// On a local worker, the only value of a Session that we care about that can
// be modified is the Status. Because of that, if LoadLocalSession is called on
// a Session that is already in the manager's data, only the Status is updated.
LoadLocalSession(ctx context.Context, id string, workerId string) (Session, error)
// DeleteLocalSession removes all sessions with the provided id from the
// local manager. If ids are passed in which do not exist in the manager
// no error is returned.
DeleteLocalSession([]string)
// RequestCloseConnections sends connection close requests to the controller,
// and sets close times within the worker. It should be called during the worker
// status 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 ids mapped to their individual session ids.
RequestCloseConnections(context.Context, map[string]string) bool
}
type manager struct {
controllerSessionConn pbs.SessionServiceClient
sessionMap sync.Map
}
var _ Manager = (*manager)(nil)
// NewManager returns a *Manager which uses the provided ServiceServiceClient to
// perform actions on Sessions and Connections on the Controller.
func NewManager(client pbs.SessionServiceClient) (*manager, error) {
if isNil(client) {
return nil, fmt.Errorf("SessionServiceClient is nil")
}
return &manager{
controllerSessionConn: client,
}, nil
}
func (m *manager) Get(id string) Session {
if s, ok := m.sessionMap.Load(id); ok {
return s.(*sess)
}
return nil
}
func (m *manager) ForEachLocalSession(f func(Session) bool) {
// TODO: Periodically clean this up. We can't rely on things in here but
// not in cancellation because they could be on the way to being
// established. However, since cert lifetimes are short, we can simply range
// through and remove values that are expired.
m.sessionMap.Range(func(_, value any) bool {
s, ok := value.(Session)
if !ok {
return false
}
return f(s)
})
}
func (m *manager) LoadLocalSession(ctx context.Context, id string, workerId string) (Session, error) {
const op = "session.(*manager).LoadLocalSession"
switch {
case id == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "id is not set")
case workerId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "workerId is not set")
}
resp, err := m.controllerSessionConn.LookupSession(ctx, &pbs.LookupSessionRequest{
SessionId: id,
WorkerId: workerId,
})
if err != nil {
return nil, err
}
s, err := newSess(m.controllerSessionConn, resp)
if err != nil {
return nil, err
}
actualSessRaw, loaded := m.sessionMap.LoadOrStore(s.GetId(), s)
if !loaded {
return s, nil
}
// Update the response to the latest
actualSess := actualSessRaw.(*sess)
actualSess.ApplyLocalStatus(s.GetStatus())
return actualSess, nil
}
func (m *manager) DeleteLocalSession(sessIds []string) {
for _, s := range sessIds {
m.sessionMap.Delete(s)
}
}
func (m *manager) RequestCloseConnections(ctx context.Context, closeInfo map[string]string) bool {
return closeConnections(ctx, m.controllerSessionConn, m, closeInfo)
}
func isNil(i interface{}) bool {
if i == nil {
return true
}
switch reflect.TypeOf(i).Kind() {
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
return reflect.ValueOf(i).IsNil()
}
return false
}
@@ -0,0 +1,474 @@
package session
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"errors"
"fmt"
"math/big"
"net"
"sync"
"testing"
"time"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/targets"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestManager_Get(t *testing.T) {
mockSessionClient := pbs.NewMockSessionServiceClient()
mockSessionClient.LookupSessionFn = func(context.Context, *pbs.LookupSessionRequest) (*pbs.LookupSessionResponse, error) {
return &pbs.LookupSessionResponse{
Authorization: &targets.SessionAuthorizationData{
SessionId: "foo",
Certificate: createTestCert(t),
},
Version: 1,
Expiration: timestamppb.New(time.Now().Add(time.Hour)),
Status: pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING,
}, nil
}
manager, err := NewManager(mockSessionClient)
require.NoError(t, err)
_, err = manager.LoadLocalSession(context.Background(), "foo", "worker id")
require.NoError(t, err)
assert.NotNil(t, manager.Get("foo"))
assert.Nil(t, manager.Get("unknown"))
}
func TestManager_DeleteLocalSession(t *testing.T) {
mockSessionClient := pbs.NewMockSessionServiceClient()
mockSessionClient.LookupSessionFn = func(_ context.Context, req *pbs.LookupSessionRequest) (*pbs.LookupSessionResponse, error) {
return &pbs.LookupSessionResponse{
Authorization: &targets.SessionAuthorizationData{
SessionId: req.GetSessionId(),
Certificate: createTestCert(t),
},
Version: 1,
Expiration: timestamppb.New(time.Now().Add(time.Hour)),
Status: pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING,
}, nil
}
manager, err := NewManager(mockSessionClient)
require.NoError(t, err)
_, err = manager.LoadLocalSession(context.Background(), "foo", "worker id")
require.NoError(t, err)
require.NotNil(t, manager.Get("foo"))
manager.DeleteLocalSession([]string{"foo"})
assert.Nil(t, manager.Get("foo"))
// A second call to DeleteLocalSession is ok.
manager.DeleteLocalSession([]string{"foo"})
assert.Nil(t, manager.Get("foo"))
}
func TestManager_RequestCloseConnections(t *testing.T) {
ctx := context.Background()
mockSessionClient := pbs.NewMockSessionServiceClient()
manager, err := NewManager(mockSessionClient)
require.NoError(t, err)
assert.False(t, manager.RequestCloseConnections(ctx, nil))
assert.False(t, manager.RequestCloseConnections(ctx, map[string]string{}))
mockSessionClient.LookupSessionFn = func(_ context.Context, req *pbs.LookupSessionRequest) (*pbs.LookupSessionResponse, error) {
return &pbs.LookupSessionResponse{
Authorization: &targets.SessionAuthorizationData{
SessionId: req.GetSessionId(),
Certificate: createTestCert(t),
},
Version: 1,
Expiration: timestamppb.New(time.Now().Add(time.Hour)),
Status: pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING,
}, nil
}
session1, err := manager.LoadLocalSession(ctx, "sess1", "worker id")
require.NoError(t, err)
session2, err := manager.LoadLocalSession(ctx, "sess2", "worker id")
require.NoError(t, err)
session3, err := manager.LoadLocalSession(ctx, "sess3", "worker id")
require.NoError(t, err)
mockSessionClient.CloseConnectionFn = func(context.Context, *pbs.CloseConnectionRequest) (*pbs.CloseConnectionResponse, error) {
return nil, errors.New("error")
}
assert.False(t, manager.RequestCloseConnections(ctx, map[string]string{"connection id": session1.GetId()}))
mockSessionClient.CloseConnectionFn = func(_ context.Context, req *pbs.CloseConnectionRequest) (*pbs.CloseConnectionResponse, error) {
var data []*pbs.CloseConnectionResponseData
for _, r := range req.GetCloseRequestData() {
data = append(data, &pbs.CloseConnectionResponseData{
ConnectionId: r.GetConnectionId(),
Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED,
})
}
return &pbs.CloseConnectionResponse{CloseResponseData: data}, nil
}
// There is no connection information yet for this connection.
assert.False(t, manager.RequestCloseConnections(ctx, map[string]string{
"random_connection_id": session1.GetId(),
}))
// Load the connection info into the local storage
mockSessionClient.AuthorizeConnectionFn = func(_ context.Context, req *pbs.AuthorizeConnectionRequest) (*pbs.AuthorizeConnectionResponse, error) {
return &pbs.AuthorizeConnectionResponse{
ConnectionId: fmt.Sprintf("connection_%s", req.GetSessionId()),
Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_AUTHORIZED,
ConnectionsLeft: -1,
}, nil
}
_, cancelFn := context.WithCancel(ctx)
c1, _, err := session1.RequestAuthorizeConnection(ctx, "worker id", cancelFn)
require.NoError(t, err)
assert.True(t, manager.RequestCloseConnections(ctx, map[string]string{
c1.Id: session1.GetId(),
}))
c2, _, err := session2.RequestAuthorizeConnection(ctx, "worker id", cancelFn)
require.NoError(t, err)
c3, _, err := session3.RequestAuthorizeConnection(ctx, "worker id", cancelFn)
require.NoError(t, err)
assert.True(t, manager.RequestCloseConnections(ctx, map[string]string{
c2.Id: session2.GetId(),
c3.Id: session3.GetId(),
}))
}
func TestManager_LoadLocalSession(t *testing.T) {
mockSessionClient := pbs.NewMockSessionServiceClient()
errorCases := []struct {
name string
inId string
inWorkerId string
controllerResponse func() (*pbs.LookupSessionResponse, error)
wantError string
}{
{
name: "no id",
inWorkerId: "worker id",
wantError: "id is not set",
},
{
name: "no worker id",
inId: "id",
wantError: "workerId is not set",
},
{
name: "controller error",
inId: "id",
inWorkerId: "worker id",
controllerResponse: func() (*pbs.LookupSessionResponse, error) {
return nil, errors.New("some error")
},
wantError: "some error",
},
{
name: "cant parse cert",
inId: "id",
inWorkerId: "worker id",
controllerResponse: func() (*pbs.LookupSessionResponse, error) {
return &pbs.LookupSessionResponse{
Authorization: &targets.SessionAuthorizationData{
SessionId: "foo",
Certificate: []byte("unparseable"),
},
Version: 1,
Expiration: timestamppb.New(time.Now().Add(time.Hour)),
Status: pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING,
}, nil
},
wantError: "error parsing session certificate",
},
{
name: "expired session",
inId: "id",
inWorkerId: "worker id",
controllerResponse: func() (*pbs.LookupSessionResponse, error) {
return &pbs.LookupSessionResponse{
Authorization: &targets.SessionAuthorizationData{
SessionId: "foo",
Certificate: createTestCert(t),
},
Version: 1,
Expiration: timestamppb.New(time.Now().Add(-time.Hour)),
Status: pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING,
}, nil
},
wantError: "session is expired",
},
}
for _, tc := range errorCases {
t.Run(tc.name, func(t *testing.T) {
mockSessionClient.LookupSessionFn = func(context.Context, *pbs.LookupSessionRequest) (*pbs.LookupSessionResponse, error) {
return tc.controllerResponse()
}
manager, err := NewManager(mockSessionClient)
require.NoError(t, err)
s, err := manager.LoadLocalSession(context.Background(), tc.inId, tc.inWorkerId)
require.Error(t, err)
assert.Nil(t, s)
assert.ErrorContains(t, err, tc.wantError)
})
}
t.Run("success", func(t *testing.T) {
expirationTime := time.Now().Add(time.Hour).UTC()
mockSessionClient.LookupSessionFn = func(context.Context, *pbs.LookupSessionRequest) (*pbs.LookupSessionResponse, error) {
return &pbs.LookupSessionResponse{
Authorization: &targets.SessionAuthorizationData{
SessionId: "foo",
Certificate: createTestCert(t),
},
ConnectionLimit: -1,
Version: 1,
Expiration: timestamppb.New(expirationTime),
Status: pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING,
}, nil
}
manager, err := NewManager(mockSessionClient)
require.NoError(t, err)
s, err := manager.LoadLocalSession(context.Background(), "foo", "worker id")
require.NoError(t, err)
assert.Equal(t, "foo", s.GetId())
assert.Equal(t, pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING, s.GetStatus())
assert.Empty(t, s.GetLocalConnections())
assert.Equal(t, int32(-1), s.GetConnectionLimit())
assert.Equal(t, expirationTime, s.GetExpiration())
})
}
func createTestCert(t *testing.T) []byte {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
template := &x509.Certificate{
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageCertSign,
SerialNumber: big.NewInt(0),
NotBefore: time.Now().Add(-30 * time.Second),
NotAfter: time.Now().Add(5 * time.Minute),
BasicConstraintsValid: true,
IsCA: true,
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
DNSNames: []string{"/tmp/boundary-opslistener-test0.sock", "/tmp/boundary-opslistener-test1.sock"},
}
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv)
require.NoError(t, err)
return certBytes
}
func TestWorkerSetCloseTimeForResponse(t *testing.T) {
cases := []struct {
name string
sessionCloseInfo map[string][]*pbs.CloseConnectionResponseData
sessionInfoMap func() sync.Map
expected []string
expectedClosed map[string]struct{}
expectedErr []error
}{
{
name: "basic",
sessionCloseInfo: map[string][]*pbs.CloseConnectionResponseData{
"one": {
{ConnectionId: "foo", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED},
},
"two": {
{ConnectionId: "bar", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED},
},
},
sessionInfoMap: func() sync.Map {
var m sync.Map
m.Store("one", &sess{
resp: &pbs.LookupSessionResponse{Authorization: &targets.SessionAuthorizationData{
SessionId: "one",
}},
connInfoMap: map[string]*ConnInfo{
"foo": {Id: "foo"},
},
})
m.Store("two", &sess{
resp: &pbs.LookupSessionResponse{Authorization: &targets.SessionAuthorizationData{
SessionId: "two",
}},
connInfoMap: map[string]*ConnInfo{
"bar": {Id: "bar"},
},
})
m.Store("three", &sess{
resp: &pbs.LookupSessionResponse{Authorization: &targets.SessionAuthorizationData{
SessionId: "three",
}},
connInfoMap: map[string]*ConnInfo{
"baz": {Id: "baz"},
},
})
return m
},
expected: []string{"foo", "bar"},
expectedClosed: map[string]struct{}{
"foo": {},
"bar": {},
},
},
{
name: "not closed",
sessionCloseInfo: map[string][]*pbs.CloseConnectionResponseData{
"one": {
{ConnectionId: "foo", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED},
},
"two": {
{ConnectionId: "bar", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CONNECTED},
},
},
sessionInfoMap: func() sync.Map {
var m sync.Map
m.Store("one", &sess{
resp: &pbs.LookupSessionResponse{Authorization: &targets.SessionAuthorizationData{
SessionId: "one",
}},
connInfoMap: map[string]*ConnInfo{
"foo": {Id: "foo"},
},
})
m.Store("two", &sess{
resp: &pbs.LookupSessionResponse{Authorization: &targets.SessionAuthorizationData{
SessionId: "two",
}},
connInfoMap: map[string]*ConnInfo{
"bar": {Id: "bar"},
},
})
return m
},
expected: []string{"foo"},
expectedClosed: map[string]struct{}{
"foo": {},
},
},
{
name: "missing session",
sessionCloseInfo: map[string][]*pbs.CloseConnectionResponseData{
"one": {
{ConnectionId: "foo", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED},
},
"two": {
{ConnectionId: "bar", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED},
},
},
sessionInfoMap: func() sync.Map {
var m sync.Map
m.Store("one", &sess{
resp: &pbs.LookupSessionResponse{Authorization: &targets.SessionAuthorizationData{
SessionId: "one",
}},
connInfoMap: map[string]*ConnInfo{
"foo": {Id: "foo"},
},
})
return m
},
expected: []string{"foo"},
expectedClosed: map[string]struct{}{
"foo": {},
},
expectedErr: []error{
errors.New(`could not find session ID "two" in local state after closing connections`),
},
},
{
name: "missing connection",
sessionCloseInfo: map[string][]*pbs.CloseConnectionResponseData{
"one": {
{ConnectionId: "foo", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED},
},
"two": {
{ConnectionId: "bar", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED},
},
},
sessionInfoMap: func() sync.Map {
var m sync.Map
m.Store("one", &sess{
resp: &pbs.LookupSessionResponse{Authorization: &targets.SessionAuthorizationData{
SessionId: "one",
}},
connInfoMap: map[string]*ConnInfo{
"foo": {Id: "foo"},
},
})
m.Store("two", &sess{
resp: &pbs.LookupSessionResponse{Authorization: &targets.SessionAuthorizationData{
SessionId: "two",
}},
})
return m
},
expected: []string{"foo"},
expectedClosed: map[string]struct{}{
"foo": {},
},
expectedErr: []error{
errors.New(`could not find connection ID "bar" for session ID "two" in local state`),
},
},
{
name: "empty",
sessionCloseInfo: make(map[string][]*pbs.CloseConnectionResponseData),
sessionInfoMap: func() sync.Map {
var m sync.Map
m.Store("one", &sess{
resp: &pbs.LookupSessionResponse{Authorization: &targets.SessionAuthorizationData{
SessionId: "one",
}},
connInfoMap: map[string]*ConnInfo{
"foo": {Id: "foo"},
},
})
return m
},
expected: []string{},
expectedClosed: map[string]struct{}{},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
manager := &manager{}
manager.sessionMap = tc.sessionInfoMap()
actual, actualErr := setCloseTimeForResponse(manager, tc.sessionCloseInfo)
// Assert all close times were set
manager.ForEachLocalSession(func(value Session) bool {
t.Helper()
for _, ci := range value.GetLocalConnections() {
if _, ok := tc.expectedClosed[ci.Id]; ok {
require.NotEqual(time.Time{}, ci.CloseTime)
} else {
require.Equal(time.Time{}, ci.CloseTime)
}
}
return true
})
// Assert return values
require.ElementsMatch(tc.expected, actual)
require.ElementsMatch(tc.expectedErr, actualErr)
})
}
}
+307 -54
View File
@@ -2,7 +2,7 @@ package session
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"sync"
@@ -22,26 +22,291 @@ var errMakeSessionCloseInfoNilCloseInfo = errors.New("nil closeInfo supplied to
// ConnInfo defines the information about a connection attached to a session
type ConnInfo struct {
Id string
ConnCtx context.Context
ConnCancel context.CancelFunc
Status pbs.CONNECTIONSTATUS
CloseTime time.Time
Id string
Status pbs.CONNECTIONSTATUS
// The context.CancelFunc for the proxy connection. Calling this function
// closes the proxy connection.
connCtxCancelFunc context.CancelFunc
// The time the controller has successfully reported that this connection is
// closed.
CloseTime time.Time
}
// Info defines the information about a session
type Info struct {
sync.RWMutex
Id string
SessionTls *tls.Config
Status pbs.SESSIONSTATUS
LookupSessionResponse *pbs.LookupSessionResponse
ConnInfoMap map[string]*ConnInfo
// 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
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
// 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) (ConnInfo, 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
}
// Activate is a helper worker function that sends session activation request to the
type sess struct {
lock sync.RWMutex
client pbs.SessionServiceClient
connInfoMap map[string]*ConnInfo
resp *pbs.LookupSessionResponse
status pbs.SESSIONSTATUS
cert *x509.Certificate
}
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,
}
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) ApplyLocalStatus(st pbs.SESSIONSTATUS) {
s.lock.Lock()
defer s.lock.Unlock()
s.status = st
}
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,
}
}
return res
}
func (s *sess) GetTofuToken() string {
return s.resp.GetTofuToken()
}
func (s *sess) GetConnectionLimit() int32 {
return s.resp.GetConnectionLimit()
}
func (s *sess) GetEndpoint() string {
return s.resp.GetEndpoint()
}
func (s *sess) GetCredentials() []*pbs.Credential {
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 {
return s.resp.GetExpiration().AsTime()
}
func (s *sess) GetCertificate() *x509.Certificate {
return s.cert
}
func (s *sess) GetPrivateKey() []byte {
return s.resp.GetAuthorization().GetPrivateKey()
}
func (s *sess) GetId() string {
return s.resp.GetAuthorization().GetSessionId()
}
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.ApplyLocalStatus(st)
return nil
}
func (s *sess) RequestAuthorizeConnection(ctx context.Context, workerId string, connCancel context.CancelFunc) (ConnInfo, int32, error) {
switch {
case connCancel == nil:
return ConnInfo{}, 0, errors.New("the provided context.CancelFunc was nil")
case workerId == "":
return ConnInfo{}, 0, errors.New("worker id is empty")
}
ci, connsLeft, err := authorizeConnection(ctx, s.client, workerId, s.GetId())
if err != nil {
return ConnInfo{}, connsLeft, err
}
ci.connCtxCancelFunc = connCancel
s.lock.Lock()
defer s.lock.Unlock()
s.connInfoMap[ci.Id] = ci
return ConnInfo{
Id: ci.Id,
Status: ci.Status,
CloseTime: ci.CloseTime,
}, connsLeft, err
}
func (s *sess) RequestConnectConnection(ctx context.Context, info *pbs.ConnectConnectionRequest) error {
st, err := connectConnection(ctx, s.client, info)
if err != nil {
return err
}
s.ApplyLocalConnectionStatus(info.GetConnectionId(), st)
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 {
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 {
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
}
// 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) {
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,
@@ -53,9 +318,9 @@ func Activate(ctx context.Context, sessClient pbs.SessionServiceClient, sessionI
return resp.GetStatus(), nil
}
// Cancel is a helper worker function that sends session cancellation request to the
// 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) {
func cancel(ctx context.Context, sessClient pbs.SessionServiceClient, sessionId string) (pbs.SESSIONSTATUS, error) {
resp, err := sessClient.CancelSession(ctx, &pbs.CancelSessionRequest{
SessionId: sessionId,
})
@@ -65,10 +330,10 @@ func Cancel(ctx context.Context, sessClient pbs.SessionServiceClient, sessionId
return resp.GetStatus(), nil
}
// AuthorizeConnection is a helper worker function that sends connection
// authorizeConnection is a helper worker function that sends connection
// authorization 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.
func AuthorizeConnection(ctx context.Context, sessClient pbs.SessionServiceClient, workerId, sessionId string) (*ConnInfo, int32, error) {
func authorizeConnection(ctx context.Context, sessClient pbs.SessionServiceClient, workerId, sessionId string) (*ConnInfo, int32, error) {
resp, err := sessClient.AuthorizeConnection(ctx, &pbs.AuthorizeConnectionRequest{
SessionId: sessionId,
WorkerId: workerId,
@@ -83,10 +348,10 @@ func AuthorizeConnection(ctx context.Context, sessClient pbs.SessionServiceClien
}, resp.GetConnectionsLeft(), nil
}
// ConnectConnection is a helper worker function that sends connection
// 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) {
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
@@ -99,6 +364,8 @@ func ConnectConnection(ctx context.Context, sessClient pbs.SessionServiceClient,
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)
@@ -112,7 +379,7 @@ func closeConnection(ctx context.Context, sessClient pbs.SessionServiceClient, r
return resp, nil
}
// CloseConnections is a helper worker function that sends connection close
// 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 status loop and on connection exit on the proxy.
//
@@ -120,9 +387,9 @@ func closeConnection(ctx context.Context, sessClient pbs.SessionServiceClient, r
// errors. Individual events will be sent for the errors if there are any.
//
// closeInfo is a map of connections mapped to their individual session.
func CloseConnections(ctx context.Context, sessClient pbs.SessionServiceClient, sessionInfo *sync.Map, closeInfo map[string]string) bool {
const op = "session.CloseConnections"
if closeInfo == nil {
func closeConnections(ctx context.Context, sessClient pbs.SessionServiceClient, sManager Manager, closeInfo map[string]string) 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
@@ -160,7 +427,7 @@ func CloseConnections(ctx context.Context, sessClient pbs.SessionServiceClient,
}
// Mark connections as closed
_, errs := setCloseTimeForResponse(sessionInfo, sessionCloseInfo)
_, 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"))
@@ -192,7 +459,7 @@ func makeCloseConnectionRequest(closeInfo map[string]string) *pbs.CloseConnectio
}
}
// makeSessionCloseInfo takes the response from CloseConnections and
// 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
@@ -245,41 +512,27 @@ func makeFakeSessionCloseInfo(
// 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(sessionInfo *sync.Map, sessionCloseInfo map[string][]*pbs.CloseConnectionResponseData) ([]string, []error) {
func setCloseTimeForResponse(sManager Manager, sessionCloseInfo map[string][]*pbs.CloseConnectionResponseData) ([]string, []error) {
closedIds := make([]string, 0)
var errors []error
var result []error
for sessionId, responses := range sessionCloseInfo {
siRaw, ok := sessionInfo.Load(sessionId)
if !ok {
errors = append(errors, fmt.Errorf("could not find session ID %q in local state after closing connections", sessionId))
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
}
si := siRaw.(*Info)
si.Lock()
connStatus := make(map[string]pbs.CONNECTIONSTATUS, len(responses))
for _, response := range responses {
ci, ok := si.ConnInfoMap[response.GetConnectionId()]
if !ok {
errors = append(errors,
fmt.Errorf(
"could not find connection ID %q for session ID %q in local state after closing connections",
response.GetConnectionId(),
sessionId,
),
)
if err := si.ApplyLocalConnectionStatus(response.GetConnectionId(), response.GetStatus()); err != nil {
result = append(result, err)
continue
}
ci.Status = response.GetStatus()
if ci.Status == pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED {
ci.CloseTime = time.Now()
closedIds = append(closedIds, ci.Id)
connStatus[response.GetConnectionId()] = response.GetStatus()
if response.GetStatus() == pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED {
closedIds = append(closedIds, response.GetConnectionId())
}
}
si.Unlock()
}
return closedIds, errors
return closedIds, result
}
+165 -188
View File
@@ -1,16 +1,179 @@
package session
import (
"errors"
"sync"
"context"
"fmt"
"testing"
"time"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/internal/session"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSession_ApplyLocalStatus(t *testing.T) {
sess := &sess{
status: pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING,
}
for _, s := range []pbs.SESSIONSTATUS{
pbs.SESSIONSTATUS_SESSIONSTATUS_ACTIVE,
pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING,
pbs.SESSIONSTATUS_SESSIONSTATUS_CANCELING,
pbs.SESSIONSTATUS_SESSIONSTATUS_TERMINATED,
} {
sess.ApplyLocalStatus(s)
assert.Equal(t, s, sess.GetStatus())
}
}
func TestSession_CancelAllLocalConnections(t *testing.T) {
var closedContextCalled []string
cancelFn := func(id string) context.CancelFunc {
return func() {
closedContextCalled = append(closedContextCalled, id)
}
}
connInfo := map[string]*ConnInfo{
"1": {
Id: "1",
connCtxCancelFunc: cancelFn("1"),
},
"2": {
Id: "2",
connCtxCancelFunc: cancelFn("2"),
},
"3": {
Id: "3",
connCtxCancelFunc: cancelFn("3"),
CloseTime: time.Now(),
},
}
sess := &sess{
connInfoMap: connInfo,
}
assert.ElementsMatch(t, sess.CancelAllLocalConnections(), []string{"1", "2"})
// We can call the cancel context multiple times, even if it was marked
// closed previously.
assert.ElementsMatch(t, closedContextCalled, []string{"1", "2", "3"})
}
func TestSession_CancelOpenLocalConnections(t *testing.T) {
var closedContextCalled []string
cancelFn := func(id string) context.CancelFunc {
return func() {
closedContextCalled = append(closedContextCalled, id)
}
}
connInfo := map[string]*ConnInfo{
"1": {
Id: "1",
connCtxCancelFunc: cancelFn("1"),
Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED,
},
"2": {
Id: "2",
connCtxCancelFunc: cancelFn("2"),
Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED,
CloseTime: time.Now(),
},
"3": {
Id: "3",
connCtxCancelFunc: cancelFn("3"),
Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CONNECTED,
},
}
sess := &sess{
connInfoMap: connInfo,
}
assert.ElementsMatch(t, sess.CancelOpenLocalConnections(), []string{"1"})
// We call the cancel context multiple times, even if it was marked
// closed previously like connection 2 was (by setting CloseTime)
assert.ElementsMatch(t, closedContextCalled, []string{"1", "2"})
}
func TestSession_RequestActivate(t *testing.T) {
mockClient := pbs.NewMockSessionServiceClient()
mockClient.ActivateSessionFn = func(context.Context, *pbs.ActivateSessionRequest) (*pbs.ActivateSessionResponse, error) {
return nil, fmt.Errorf("test error")
}
sess := &sess{
client: mockClient,
resp: &pbs.LookupSessionResponse{
TofuToken: "tofu",
Version: 1,
Status: pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING,
ConnectionLimit: -1,
},
status: pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING,
}
assert.Error(t, sess.RequestActivate(context.Background(), "tofu"))
mockClient.ActivateSessionFn = func(context.Context, *pbs.ActivateSessionRequest) (*pbs.ActivateSessionResponse, error) {
return &pbs.ActivateSessionResponse{Status: pbs.SESSIONSTATUS_SESSIONSTATUS_ACTIVE}, nil
}
assert.NoError(t, sess.RequestActivate(context.Background(), "tofu"))
assert.Equal(t, pbs.SESSIONSTATUS_SESSIONSTATUS_ACTIVE, sess.GetStatus())
}
func TestSession_RequestCancel(t *testing.T) {
mockClient := pbs.NewMockSessionServiceClient()
mockClient.CancelSessionFn = func(context.Context, *pbs.CancelSessionRequest) (*pbs.CancelSessionResponse, error) {
return nil, fmt.Errorf("test error")
}
sess := &sess{
client: mockClient,
resp: &pbs.LookupSessionResponse{
TofuToken: "tofu",
Version: 1,
Status: pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING,
ConnectionLimit: -1,
},
status: pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING,
}
assert.Error(t, sess.RequestCancel(context.Background()))
mockClient.CancelSessionFn = func(context.Context, *pbs.CancelSessionRequest) (*pbs.CancelSessionResponse, error) {
return &pbs.CancelSessionResponse{Status: pbs.SESSIONSTATUS_SESSIONSTATUS_CANCELING}, nil
}
assert.NoError(t, sess.RequestCancel(context.Background()))
assert.Equal(t, pbs.SESSIONSTATUS_SESSIONSTATUS_CANCELING, sess.GetStatus())
}
func TestSession_RequestAuthorizeConnection(t *testing.T) {
mockClient := pbs.NewMockSessionServiceClient()
mockClient.AuthorizeConnectionFn = func(ctx context.Context, request *pbs.AuthorizeConnectionRequest) (*pbs.AuthorizeConnectionResponse, error) {
return nil, fmt.Errorf("test error")
}
sess := &sess{
client: mockClient,
connInfoMap: make(map[string]*ConnInfo),
resp: &pbs.LookupSessionResponse{
TofuToken: "tofu",
Version: 1,
Status: pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING,
ConnectionLimit: -1,
},
status: pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING,
}
_, cancel := context.WithCancel(context.Background())
conn, _, err := sess.RequestAuthorizeConnection(context.Background(), "workerid", cancel)
require.Error(t, err)
assert.Equal(t, ConnInfo{}, conn)
mockClient.AuthorizeConnectionFn = func(ctx context.Context, request *pbs.AuthorizeConnectionRequest) (*pbs.AuthorizeConnectionResponse, error) {
return &pbs.AuthorizeConnectionResponse{
ConnectionId: "conn1",
Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_AUTHORIZED,
ConnectionsLeft: -1,
}, nil
}
conn, left, err := sess.RequestAuthorizeConnection(context.Background(), "workerid", cancel)
require.NoError(t, err)
assert.Equal(t, ConnInfo{Id: "conn1", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_AUTHORIZED}, conn)
assert.Equal(t, int32(-1), left)
}
func TestWorkerMakeCloseConnectionRequest(t *testing.T) {
require := require.New(t)
in := map[string]string{"foo": "one", "bar": "two"}
@@ -95,189 +258,3 @@ func TestMakeFakeSessionCloseInfoEmpty(t *testing.T) {
actual,
)
}
func TestWorkerSetCloseTimeForResponse(t *testing.T) {
cases := []struct {
name string
sessionCloseInfo map[string][]*pbs.CloseConnectionResponseData
sessionInfoMap func() *sync.Map
expected []string
expectedClosed map[string]struct{}
expectedErr []error
}{
{
name: "basic",
sessionCloseInfo: map[string][]*pbs.CloseConnectionResponseData{
"one": {
{ConnectionId: "foo", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED},
},
"two": {
{ConnectionId: "bar", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED},
},
},
sessionInfoMap: func() *sync.Map {
m := new(sync.Map)
m.Store("one", &Info{
Id: "one",
ConnInfoMap: map[string]*ConnInfo{
"foo": {Id: "foo"},
},
})
m.Store("two", &Info{
Id: "two",
ConnInfoMap: map[string]*ConnInfo{
"bar": {Id: "bar"},
},
})
m.Store("three", &Info{
Id: "three",
ConnInfoMap: map[string]*ConnInfo{
"baz": {Id: "baz"},
},
})
return m
},
expected: []string{"foo", "bar"},
expectedClosed: map[string]struct{}{
"foo": {},
"bar": {},
},
},
{
name: "not closed",
sessionCloseInfo: map[string][]*pbs.CloseConnectionResponseData{
"one": {
{ConnectionId: "foo", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED},
},
"two": {
{ConnectionId: "bar", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CONNECTED},
},
},
sessionInfoMap: func() *sync.Map {
m := new(sync.Map)
m.Store("one", &Info{
Id: "one",
ConnInfoMap: map[string]*ConnInfo{
"foo": {Id: "foo"},
},
})
m.Store("two", &Info{
Id: "two",
ConnInfoMap: map[string]*ConnInfo{
"bar": {Id: "bar"},
},
})
return m
},
expected: []string{"foo"},
expectedClosed: map[string]struct{}{
"foo": {},
},
},
{
name: "missing session",
sessionCloseInfo: map[string][]*pbs.CloseConnectionResponseData{
"one": {
{ConnectionId: "foo", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED},
},
"two": {
{ConnectionId: "bar", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED},
},
},
sessionInfoMap: func() *sync.Map {
m := new(sync.Map)
m.Store("one", &Info{
Id: "one",
ConnInfoMap: map[string]*ConnInfo{
"foo": {Id: "foo"},
},
})
return m
},
expected: []string{"foo"},
expectedClosed: map[string]struct{}{
"foo": {},
},
expectedErr: []error{
errors.New(`could not find session ID "two" in local state after closing connections`),
},
},
{
name: "missing connection",
sessionCloseInfo: map[string][]*pbs.CloseConnectionResponseData{
"one": {
{ConnectionId: "foo", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED},
},
"two": {
{ConnectionId: "bar", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED},
},
},
sessionInfoMap: func() *sync.Map {
m := new(sync.Map)
m.Store("one", &Info{
Id: "one",
ConnInfoMap: map[string]*ConnInfo{
"foo": {Id: "foo"},
},
})
m.Store("two", &Info{Id: "two"})
return m
},
expected: []string{"foo"},
expectedClosed: map[string]struct{}{
"foo": {},
},
expectedErr: []error{
errors.New(`could not find connection ID "bar" for session ID "two" in local state after closing connections`),
},
},
{
name: "empty",
sessionCloseInfo: make(map[string][]*pbs.CloseConnectionResponseData),
sessionInfoMap: func() *sync.Map {
m := new(sync.Map)
m.Store("one", &Info{
Id: "one",
ConnInfoMap: map[string]*ConnInfo{
"foo": {Id: "foo"},
},
})
return m
},
expected: []string{},
expectedClosed: map[string]struct{}{},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
sessionInfoMap := tc.sessionInfoMap()
actual, actualErr := setCloseTimeForResponse(sessionInfoMap, tc.sessionCloseInfo)
// Assert all close times were set
sessionInfoMap.Range(func(key, value interface{}) bool {
t.Helper()
for _, ci := range value.(*Info).ConnInfoMap {
if _, ok := tc.expectedClosed[ci.Id]; ok {
require.NotEqual(time.Time{}, ci.CloseTime)
} else {
require.Equal(time.Time{}, ci.CloseTime)
}
}
return true
})
// Assert return values
require.ElementsMatch(tc.expected, actual)
require.ElementsMatch(tc.expectedErr, actualErr)
})
}
}
+37 -90
View File
@@ -23,7 +23,7 @@ type LastStatusInformation struct {
LastCalculatedUpstreams []string
}
func (w *Worker) startStatusTicking(cancelCtx context.Context) {
func (w *Worker) startStatusTicking(cancelCtx context.Context, sessionManager session.Manager) {
const op = "worker.(Worker).startStatusTicking"
r := rand.New(rand.NewSource(time.Now().UnixNano()))
// This function exists to desynchronize calls to controllers from
@@ -47,7 +47,7 @@ func (w *Worker) startStatusTicking(cancelCtx context.Context) {
return
case <-timer.C:
w.sendWorkerStatus(cancelCtx)
w.sendWorkerStatus(cancelCtx, sessionManager)
timer.Reset(getRandomInterval())
}
}
@@ -91,7 +91,7 @@ func (w *Worker) WaitForNextSuccessfulStatusUpdate() error {
return nil
}
func (w *Worker) sendWorkerStatus(cancelCtx context.Context) {
func (w *Worker) sendWorkerStatus(cancelCtx context.Context, sessionManager session.Manager) {
const op = "worker.(Worker).sendWorkerStatus"
// If we've never managed to successfully authenticate then we won't have
@@ -107,20 +107,17 @@ func (w *Worker) sendWorkerStatus(cancelCtx context.Context) {
var activeJobs []*pbs.JobStatus
// Range over known sessions and collect info
w.sessionInfoMap.Range(func(key, value interface{}) bool {
sessionManager.ForEachLocalSession(func(s session.Session) bool {
var jobInfo pbs.SessionJobInfo
sessionId := key.(string)
si := value.(*session.Info)
si.RLock()
status := si.Status
connections := make([]*pbs.Connection, 0, len(si.ConnInfoMap))
for k, v := range si.ConnInfoMap {
status := s.GetStatus()
sessionId := s.GetId()
connections := make([]*pbs.Connection, 0, len(s.GetLocalConnections()))
for k, v := range s.GetLocalConnections() {
connections = append(connections, &pbs.Connection{
ConnectionId: k,
Status: v.Status,
})
}
si.RUnlock()
jobInfo.SessionId = sessionId
activeJobs = append(activeJobs, &pbs.JobStatus{
Job: &pbs.Job{
@@ -176,7 +173,6 @@ func (w *Worker) sendWorkerStatus(cancelCtx context.Context) {
// want to start terminating all connections as a "break glass" kind of
// scenario, as there will be no way we can really tell if these
// connections should continue to exist.
if isPastGrace, lastStatusTime, gracePeriod := w.isPastGrace(); isPastGrace {
event.WriteError(statusCtx, op,
errors.New("status error grace period has expired, canceling all sessions on worker"),
@@ -185,14 +181,9 @@ func (w *Worker) sendWorkerStatus(cancelCtx context.Context) {
// Cancel connections if grace period has expired. These Connections will be closed in the
// database on the next successful status report, or via the Controller’s dead Worker cleanup connections job.
w.sessionInfoMap.Range(func(key, value interface{}) bool {
si := value.(*session.Info)
si.Lock()
defer si.Unlock()
closedIds := w.cancelConnections(si.ConnInfoMap, true)
for _, connId := range closedIds {
event.WriteSysEvent(cancelCtx, op, "terminated connection due to status grace period expiration", "session_id", si.Id, "connection_id", connId)
sessionManager.ForEachLocalSession(func(s session.Session) bool {
for _, connId := range s.CancelAllLocalConnections() {
event.WriteSysEvent(cancelCtx, op, "terminated connection due to status grace period expiration", "session_id", s.GetId(), "connection_id", connId)
}
return true
})
@@ -231,28 +222,20 @@ func (w *Worker) sendWorkerStatus(cancelCtx context.Context) {
case pbs.JOBTYPE_JOBTYPE_SESSION:
sessInfo := request.GetJob().GetSessionInfo()
sessionId := sessInfo.GetSessionId()
siRaw, ok := w.sessionInfoMap.Load(sessionId)
if !ok {
si := sessionManager.Get(sessionId)
if si == nil {
event.WriteError(statusCtx, op, errors.New("session change requested but could not find local information for it"), event.WithInfo("session_id", sessionId))
continue
}
si := siRaw.(*session.Info)
si.Lock()
si.Status = sessInfo.GetStatus()
si.ApplyLocalStatus(sessInfo.GetStatus())
// Update connection state if there are any connections in
// the request.
for _, conn := range sessInfo.GetConnections() {
connId := conn.GetConnectionId()
connInfo, ok := si.ConnInfoMap[connId]
if !ok {
event.WriteError(statusCtx, op, errors.New("connection change requested but could not find local information for it"), event.WithInfo("connection_id", connId))
continue
if err := si.ApplyLocalConnectionStatus(conn.GetConnectionId(), conn.GetStatus()); err != nil {
event.WriteError(statusCtx, op, err, event.WithInfo("connection_id", conn.GetConnectionId()))
}
connInfo.Status = conn.GetStatus()
}
si.Unlock()
}
}
}
@@ -260,53 +243,48 @@ func (w *Worker) sendWorkerStatus(cancelCtx context.Context) {
// Standard cleanup: Run through current jobs. Cancel connections
// for any canceling session or any session that is expired.
w.cleanupConnections(cancelCtx, false)
w.cleanupConnections(cancelCtx, false, sessionManager)
}
// cleanupConnections walks all sessions and shuts down connections.
// cleanupConnections walks all sessions and shuts down all proxy connections.
// After the local connections are terminated, they are requested to be marked
// close on the controller.
// Additionally, sessions without connections are cleaned up from the
// local worker's state.
//
// Use ignoreSessionState to ignore the state checks, this closes all
// connections, regardless of whether or not the session is still
// active.
func (w *Worker) cleanupConnections(cancelCtx context.Context, ignoreSessionState bool) {
// connections, regardless of whether or not the session is still active.
func (w *Worker) cleanupConnections(cancelCtx context.Context, ignoreSessionState bool, sessionManager session.Manager) {
const op = "worker.(Worker).cleanupConnections"
closeInfo := make(map[string]string)
cleanSessionIds := make([]string, 0)
w.sessionInfoMap.Range(func(key, value interface{}) bool {
si := value.(*session.Info)
si.Lock()
defer si.Unlock()
sessionManager.ForEachLocalSession(func(s session.Session) bool {
switch {
case ignoreSessionState,
si.Status == pbs.SESSIONSTATUS_SESSIONSTATUS_CANCELING,
si.Status == pbs.SESSIONSTATUS_SESSIONSTATUS_TERMINATED,
time.Until(si.LookupSessionResponse.Expiration.AsTime()) < 0:
s.GetStatus() == pbs.SESSIONSTATUS_SESSIONSTATUS_CANCELING,
s.GetStatus() == pbs.SESSIONSTATUS_SESSIONSTATUS_TERMINATED,
time.Until(s.GetExpiration()) < 0:
// Cancel connections without regard to individual connection
// state.
closedIds := w.cancelConnections(si.ConnInfoMap, true)
closedIds := s.CancelAllLocalConnections()
for _, connId := range closedIds {
closeInfo[connId] = si.Id
event.WriteSysEvent(cancelCtx, op, "terminated connection due to cancellation or expiration", "session_id", si.Id, "connection_id", connId)
closeInfo[connId] = s.GetId()
event.WriteSysEvent(cancelCtx, op, "terminated connection due to cancellation or expiration", "session_id", s.GetId(), "connection_id", connId)
}
// CloseTime is marked by CloseConnections iff the
// status is returned for that connection as closed. If
// the session is no longer valid and all connections
// If the session is no longer valid and all connections
// are marked closed, clean up the session.
if len(closedIds) == 0 {
cleanSessionIds = append(cleanSessionIds, si.Id)
cleanSessionIds = append(cleanSessionIds, s.GetId())
}
default:
// Cancel connections *with* regard to individual connection
// state (ie: only ones that the controller has requested be
// terminated).
closedIds := w.cancelConnections(si.ConnInfoMap, false)
for _, connId := range closedIds {
closeInfo[connId] = si.Id
event.WriteSysEvent(cancelCtx, op, "terminated connection due to cancellation or expiration", "session_id", si.Id, "connection_id", connId)
for _, connId := range s.CancelOpenLocalConnections() {
closeInfo[connId] = s.GetId()
event.WriteSysEvent(cancelCtx, op, "terminated connection due to cancellation or expiration", "session_id", s.GetId(), "connection_id", connId)
}
}
@@ -319,42 +297,11 @@ func (w *Worker) cleanupConnections(cancelCtx context.Context, ignoreSessionStat
// Call out to a helper to send the connection close requests to the
// controller, and set the close time. This functionality is shared with
// post-close functionality in the proxy handler.
sessClient, err := w.ControllerSessionConn()
if err != nil {
event.WriteError(cancelCtx, op, err, event.WithInfo("failed to create controller session client, connections won't be cleaned up"))
} else {
_ = session.CloseConnections(cancelCtx, sessClient, w.sessionInfoMap, closeInfo)
}
_ = sessionManager.RequestCloseConnections(cancelCtx, closeInfo)
}
// Forget sessions where the session is expired/canceled and all
// connections are canceled and marked closed
for _, v := range cleanSessionIds {
w.sessionInfoMap.Delete(v)
}
}
// cancelConnections is run by cleanupConnections to iterate over a
// session's ConnInfoMap and close connections based on the
// connection's state (or regardless if ignoreConnectionState is
// set).
//
// The returned map and slice are the maps of connection -> session,
// and sessions to completely remove from local state, respectively.
func (w *Worker) cancelConnections(connInfoMap map[string]*session.ConnInfo, ignoreConnectionState bool) []string {
var closedIds []string
for k, v := range connInfoMap {
if v.CloseTime.IsZero() {
if !ignoreConnectionState && v.Status != pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED {
continue
}
v.ConnCancel()
closedIds = append(closedIds, k)
}
}
return closedIds
sessionManager.DeleteLocalSession(cleanSessionIds)
}
func (w *Worker) lastSuccessfulStatusTime() time.Time {
+6 -11
View File
@@ -11,7 +11,6 @@ import (
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/config"
"github.com/hashicorp/boundary/internal/daemon/worker/session"
"github.com/hashicorp/boundary/internal/db"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/internal/observability/event"
@@ -116,17 +115,13 @@ type TestConnectionInfo struct {
// See TestSessionInfo for details on how to use this info.
func (tw *TestWorker) LookupSession(id string) (TestSessionInfo, bool) {
var result TestSessionInfo
raw, ok := tw.w.sessionInfoMap.Load(id)
if !ok {
return result, false
sess := tw.w.sessionManager.Get(id)
if sess == nil {
return TestSessionInfo{}, false
}
sess := raw.(*session.Info)
sess.RLock()
defer sess.RUnlock()
conns := make(map[string]TestConnectionInfo)
for _, conn := range sess.ConnInfoMap {
for _, conn := range sess.GetLocalConnections() {
conns[conn.Id] = TestConnectionInfo{
Id: conn.Id,
Status: conn.Status,
@@ -134,8 +129,8 @@ func (tw *TestWorker) LookupSession(id string) (TestSessionInfo, bool) {
}
}
result.Id = sess.Id
result.Status = sess.Status
result.Id = sess.GetId()
result.Status = sess.GetStatus()
result.Connections = conns
return result, true
+72 -21
View File
@@ -1,40 +1,72 @@
package worker
import (
"sync"
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"math/big"
"net"
"testing"
"time"
"github.com/hashicorp/boundary/internal/daemon/worker/session"
"github.com/hashicorp/boundary/internal/db"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/targets"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/targets/tcp"
)
func TestTestWorkerLookupSession(t *testing.T) {
require := require.New(t)
// This loads the golang reference time, see those docs for more details. We
// just use this as a stable non-zero time.
refTime, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00")
ctx := context.Background()
mockSessionClient := pbs.NewMockSessionServiceClient()
manager, err := session.NewManager(mockSessionClient)
require.NoError(err)
mockSessionClient.LookupSessionFn = func(_ context.Context, request *pbs.LookupSessionRequest) (*pbs.LookupSessionResponse, error) {
cert, _, _ := createTestCert(t)
return &pbs.LookupSessionResponse{
Authorization: &targets.SessionAuthorizationData{
SessionId: request.GetSessionId(),
Certificate: cert,
},
Version: 1,
TofuToken: "tofu",
Expiration: timestamppb.New(time.Now().Add(time.Hour)),
}, nil
}
s, err := manager.LoadLocalSession(ctx, "foo", "worker id")
require.NoError(err)
mockSessionClient.ActivateSessionFn = func(_ context.Context, _ *pbs.ActivateSessionRequest) (*pbs.ActivateSessionResponse, error) {
return &pbs.ActivateSessionResponse{Status: pbs.SESSIONSTATUS_SESSIONSTATUS_ACTIVE}, nil
}
require.NoError(s.RequestActivate(ctx, "tofu"))
mockSessionClient.AuthorizeConnectionFn = func(_ context.Context, req *pbs.AuthorizeConnectionRequest) (*pbs.AuthorizeConnectionResponse, error) {
return &pbs.AuthorizeConnectionResponse{
ConnectionId: "one",
Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_AUTHORIZED,
ConnectionsLeft: -1,
}, nil
}
_, cancelFn := context.WithCancel(context.Background())
_, _, err = s.RequestAuthorizeConnection(ctx, "worker id", cancelFn)
require.NoError(err)
require.NoError(s.ApplyLocalConnectionStatus("one", pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED))
tw := new(TestWorker)
tw.w = &Worker{
sessionInfoMap: new(sync.Map),
sessionManager: manager,
}
tw.w.sessionInfoMap.Store("foo", &session.Info{
Id: "foo",
Status: pbs.SESSIONSTATUS_SESSIONSTATUS_ACTIVE,
ConnInfoMap: map[string]*session.ConnInfo{
"one": {
Id: "one",
Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED,
CloseTime: refTime,
},
},
})
closeTime := s.GetLocalConnections()["one"].CloseTime
assert.NotZero(t, closeTime)
expected := TestSessionInfo{
Id: "foo",
Status: pbs.SESSIONSTATUS_SESSIONSTATUS_ACTIVE,
@@ -42,7 +74,7 @@ func TestTestWorkerLookupSession(t *testing.T) {
"one": {
Id: "one",
Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED,
CloseTime: refTime,
CloseTime: closeTime,
},
},
}
@@ -54,10 +86,7 @@ func TestTestWorkerLookupSession(t *testing.T) {
func TestTestWorkerLookupSessionMissing(t *testing.T) {
require := require.New(t)
tw := new(TestWorker)
tw.w = &Worker{
sessionInfoMap: new(sync.Map),
}
tw := NewTestWorker(t, nil)
actual, ok := tw.LookupSession("missing")
require.False(ok)
require.Equal(TestSessionInfo{}, actual)
@@ -87,3 +116,25 @@ func TestTestWorker_WorkerAuthStorageKms(t *testing.T) {
})
}
}
func createTestCert(t *testing.T) ([]byte, ed25519.PublicKey, ed25519.PrivateKey) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
template := &x509.Certificate{
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageCertSign,
SerialNumber: big.NewInt(0),
NotBefore: time.Now().Add(-30 * time.Second),
NotAfter: time.Now().Add(5 * time.Minute),
BasicConstraintsValid: true,
IsCA: true,
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
DNSNames: []string{"/tmp/boundary-opslistener-test0.sock", "/tmp/boundary-opslistener-test1.sock"},
}
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv)
require.NoError(t, err)
return certBytes, pub, priv
}
+98 -165
View File
@@ -52,7 +52,12 @@ type Worker struct {
tickerWg sync.WaitGroup
grpcClientConn *atomic.Value
// grpc.ClientConns are thread safe.
// See https://github.com/grpc/grpc-go/blob/master/Documentation/concurrency.md#clients
// This is exported for tests.
GrpcClientConn *grpc.ClientConn
sessionManager session.Manager
controllerStatusConn *atomic.Value
everAuthenticated *ua.Bool
@@ -61,9 +66,6 @@ type Worker struct {
controllerResolver *atomic.Value
controllerSessionConn *atomic.Value
sessionInfoMap *sync.Map
controllerMultihopConn *atomic.Value
proxyListener *base.ServerListener
@@ -100,13 +102,10 @@ func New(conf *Config) (*Worker, error) {
conf: conf,
logger: conf.Logger.Named("worker"),
started: ua.NewBool(false),
grpcClientConn: new(atomic.Value),
controllerStatusConn: new(atomic.Value),
everAuthenticated: ua.NewBool(false),
lastStatusSuccess: new(atomic.Value),
controllerResolver: new(atomic.Value),
controllerSessionConn: new(atomic.Value),
sessionInfoMap: new(sync.Map),
controllerMultihopConn: new(atomic.Value),
tags: new(atomic.Value),
updateTags: ua.NewBool(false),
@@ -115,7 +114,9 @@ func New(conf *Config) (*Worker, error) {
}
w.lastStatusSuccess.Store((*LastStatusInformation)(nil))
w.controllerResolver.Store((*manual.Resolver)(nil))
scheme := strconv.FormatInt(time.Now().UnixNano(), 36)
controllerResolver := manual.NewBuilderWithScheme(scheme)
w.controllerResolver.Store(controllerResolver)
if conf.RawConfig.Worker == nil {
conf.RawConfig.Worker = new(config.Worker)
@@ -177,10 +178,6 @@ func (w *Worker) Start() error {
return nil
}
scheme := strconv.FormatInt(time.Now().UnixNano(), 36)
controllerResolver := manual.NewBuilderWithScheme(scheme)
w.controllerResolver.Store(controllerResolver)
if w.conf.WorkerAuthKms == nil || w.conf.DevUsePkiForUpstream {
// In this section, we look for existing worker credentials. The two
// variables below store whether to create new credentials and whether
@@ -294,13 +291,20 @@ func (w *Worker) Start() error {
w.WorkerAuthCurrentKeyId.Store(currentKeyId)
}
if err := w.startListeners(); err != nil {
return fmt.Errorf("error starting worker listeners: %w", err)
}
if err := w.StartControllerConnections(); err != nil {
return fmt.Errorf("error making controller connections: %w", err)
}
var err error
w.sessionManager, err = session.NewManager(pbs.NewSessionServiceClient(w.GrpcClientConn))
if err != nil {
return fmt.Errorf("error creating session manager: %w", err)
}
if err := w.startListeners(w.sessionManager); err != nil {
return fmt.Errorf("error starting worker listeners: %w", err)
}
// Rather than deal with some of the potential error conditions for Add on
// the waitgroup vs. Done (in case a function exits immediately), we will
// always start rotation and simply exit early if we're using KMS, and
@@ -308,7 +312,7 @@ func (w *Worker) Start() error {
w.tickerWg.Add(2)
go func() {
defer w.tickerWg.Done()
w.startStatusTicking(w.baseContext)
w.startStatusTicking(w.baseContext, w.sessionManager)
}()
go func() {
defer w.tickerWg.Done()
@@ -343,7 +347,7 @@ func (w *Worker) Shutdown() error {
}
// Shut down all connections.
w.cleanupConnections(w.baseContext, true)
w.cleanupConnections(w.baseContext, true, w.sessionManager)
// Wait for next status request to succeed. Don't wait too long;
// wrap the base context in a timeout equal to our status grace
@@ -405,40 +409,6 @@ func (w *Worker) ParseAndStoreTags(incoming map[string][]string) {
w.updateTags.Store(true)
}
// GrpcClientConn returns the underlying client connection
func (w *Worker) GrpcClientConn() (*grpc.ClientConn, error) {
rawConn := w.grpcClientConn.Load()
if rawConn == nil {
return nil, errors.New("unable to load grpc client conn")
}
cc, ok := rawConn.(*grpc.ClientConn)
if !ok {
return nil, fmt.Errorf("invalid grpc client connection %T", rawConn)
}
if cc == nil {
return nil, fmt.Errorf("client connection is nil")
}
return cc, nil
}
// ControllerSessionConn returns the underlying session service client
func (w *Worker) ControllerSessionConn() (pbs.SessionServiceClient, error) {
rawConn := w.controllerSessionConn.Load()
if rawConn == nil {
return nil, errors.New("unable to load controller session service connection")
}
sessClient, ok := rawConn.(pbs.SessionServiceClient)
if !ok {
return nil, fmt.Errorf("invalid session client %T", rawConn)
}
if sessClient == nil {
return nil, fmt.Errorf("session client is nil")
}
return sessClient, nil
}
// ControllerServerCoordinationServiceConn returns the underlying server coordination service client
func (w *Worker) ControllerServerCoordinationServiceConn() (pbs.ServerCoordinationServiceClient, error) {
rawConn := w.controllerStatusConn.Load()
@@ -473,125 +443,88 @@ func (w *Worker) ControllerMultihopConn() (multihop.MultihopServiceClient, error
return multihopClient, nil
}
func (w *Worker) getSessionTls(hello *tls.ClientHelloInfo) (*tls.Config, error) {
func (w *Worker) getSessionTls(sessionManager session.Manager) func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
const op = "worker.(Worker).getSessionTls"
ctx := w.baseContext
var sessionId string
switch {
case strings.HasPrefix(hello.ServerName, globals.SessionPrefix):
sessionId = hello.ServerName
default:
for _, proto := range hello.SupportedProtos {
if strings.HasPrefix(proto, globals.SessionPrefix) {
sessionId = proto
break
return func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
ctx := w.baseContext
var sessionId string
switch {
case strings.HasPrefix(hello.ServerName, globals.SessionPrefix):
sessionId = hello.ServerName
default:
for _, proto := range hello.SupportedProtos {
if strings.HasPrefix(proto, globals.SessionPrefix) {
sessionId = proto
break
}
}
}
}
if sessionId == "" {
event.WriteSysEvent(ctx, op, "session_id not found in either SNI or ALPN protos", "server_name", hello.ServerName)
return nil, fmt.Errorf("could not find session ID in SNI or ALPN protos")
}
conn, err := w.ControllerSessionConn()
if err != nil {
event.WriteError(ctx, op, err, event.WithInfo("failed to create controller session client"))
}
lastSuccess := w.LastStatusSuccess()
if lastSuccess == nil {
event.WriteSysEvent(ctx, op, "no last status information found at session acceptance time")
return nil, fmt.Errorf("no last status information found at session acceptance time")
}
timeoutContext, cancel := context.WithTimeout(w.baseContext, session.ValidateSessionTimeout)
defer cancel()
resp, err := conn.LookupSession(timeoutContext, &pbs.LookupSessionRequest{
SessionId: sessionId,
WorkerId: lastSuccess.WorkerId,
})
if err != nil {
return nil, fmt.Errorf("error validating session: %w", err)
}
if resp.GetExpiration().AsTime().Before(time.Now()) {
return nil, fmt.Errorf("session is expired")
}
parsedCert, err := x509.ParseCertificate(resp.GetAuthorization().Certificate)
if err != nil {
return nil, fmt.Errorf("error parsing session certificate: %w", err)
}
certPool := x509.NewCertPool()
certPool.AddCert(parsedCert)
tlsConf := &tls.Config{
Certificates: []tls.Certificate{
{
Certificate: [][]byte{resp.GetAuthorization().Certificate},
PrivateKey: ed25519.PrivateKey(resp.GetAuthorization().PrivateKey),
Leaf: parsedCert,
},
},
NextProtos: []string{"http/1.1"},
MinVersion: tls.VersionTLS13,
// These two are set this way so we can make use of VerifyConnection,
// which we set on this TLS config below. We are not skipping
// verification!
ClientAuth: tls.RequireAnyClientCert,
InsecureSkipVerify: true,
}
// We disable normal DNS SAN behavior as we don't rely on DNS or IP
// addresses for security and want to avoid issues with including localhost
// etc.
verifyOpts := x509.VerifyOptions{
DNSName: sessionId,
Roots: certPool,
KeyUsages: []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth,
x509.ExtKeyUsageServerAuth,
},
}
if w.TestOverrideX509VerifyCertPool != nil {
verifyOpts.Roots = w.TestOverrideX509VerifyCertPool
}
if w.TestOverrideX509VerifyDnsName != "" {
verifyOpts.DNSName = w.TestOverrideX509VerifyDnsName
}
tlsConf.VerifyConnection = func(cs tls.ConnectionState) error {
// Go will not run this without at least one peer certificate, but
// doesn't hurt to check
if len(cs.PeerCertificates) == 0 {
return errors.New("no peer certificates provided")
if sessionId == "" {
event.WriteSysEvent(ctx, op, "session_id not found in either SNI or ALPN protos", "server_name", hello.ServerName)
return nil, fmt.Errorf("could not find session ID in SNI or ALPN protos")
}
_, err := cs.PeerCertificates[0].Verify(verifyOpts)
return err
}
si := &session.Info{
Id: resp.GetAuthorization().GetSessionId(),
SessionTls: tlsConf,
LookupSessionResponse: resp,
Status: resp.GetStatus(),
ConnInfoMap: make(map[string]*session.ConnInfo),
}
// TODO: Periodically clean this up. We can't rely on things in here but
// not in cancellation because they could be on the way to being
// established. However, since cert lifetimes are short, we can simply range
// through and remove values that are expired.
actualSiRaw, loaded := w.sessionInfoMap.LoadOrStore(sessionId, si)
if loaded {
// Update the response to the latest
actualSi := actualSiRaw.(*session.Info)
actualSi.Lock()
actualSi.LookupSessionResponse = resp
actualSi.Unlock()
}
lastSuccess := w.LastStatusSuccess()
if lastSuccess == nil {
event.WriteSysEvent(ctx, op, "no last status information found at session acceptance time")
return nil, fmt.Errorf("no last status information found at session acceptance time")
}
return tlsConf, nil
timeoutContext, cancel := context.WithTimeout(w.baseContext, session.ValidateSessionTimeout)
defer cancel()
sess, err := sessionManager.LoadLocalSession(timeoutContext, sessionId, lastSuccess.GetWorkerId())
if err != nil {
return nil, fmt.Errorf("error refreshing session: %w", err)
}
certPool := x509.NewCertPool()
certPool.AddCert(sess.GetCertificate())
tlsConf := &tls.Config{
Certificates: []tls.Certificate{
{
Certificate: [][]byte{sess.GetCertificate().Raw},
PrivateKey: ed25519.PrivateKey(sess.GetPrivateKey()),
Leaf: sess.GetCertificate(),
},
},
NextProtos: []string{"http/1.1"},
MinVersion: tls.VersionTLS13,
// These two are set this way so we can make use of VerifyConnection,
// which we set on this TLS config below. We are not skipping
// verification!
ClientAuth: tls.RequireAnyClientCert,
InsecureSkipVerify: true,
}
// We disable normal DNS SAN behavior as we don't rely on DNS or IP
// addresses for security and want to avoid issues with including localhost
// etc.
verifyOpts := x509.VerifyOptions{
DNSName: sessionId,
Roots: certPool,
KeyUsages: []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth,
x509.ExtKeyUsageServerAuth,
},
}
if w.TestOverrideX509VerifyCertPool != nil {
verifyOpts.Roots = w.TestOverrideX509VerifyCertPool
}
if w.TestOverrideX509VerifyDnsName != "" {
verifyOpts.DNSName = w.TestOverrideX509VerifyDnsName
}
tlsConf.VerifyConnection = func(cs tls.ConnectionState) error {
// Go will not run this without at least one peer certificate, but
// doesn't hurt to check
if len(cs.PeerCertificates) == 0 {
return errors.New("no peer certificates provided")
}
_, err := cs.PeerCertificates[0].Verify(verifyOpts)
return err
}
return tlsConf, nil
}
}
+10 -9
View File
@@ -5,6 +5,7 @@ import (
"sync/atomic"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"google.golang.org/grpc"
)
type workerProxyServiceServer struct {
@@ -12,16 +13,16 @@ type workerProxyServiceServer struct {
pbs.UnimplementedSessionServiceServer
scsClient *atomic.Value
ssClient *atomic.Value
ssClient pbs.SessionServiceClient
}
func NewWorkerProxyServiceServer(
cc *grpc.ClientConn,
scsClient *atomic.Value,
ssClient *atomic.Value,
) *workerProxyServiceServer {
return &workerProxyServiceServer{
scsClient: scsClient,
ssClient: ssClient,
ssClient: pbs.NewSessionServiceClient(cc),
}
}
@@ -43,25 +44,25 @@ func (ws *workerProxyServiceServer) Status(ctx context.Context, req *pbs.StatusR
}
func (ws *workerProxyServiceServer) LookupSession(ctx context.Context, req *pbs.LookupSessionRequest) (*pbs.LookupSessionResponse, error) {
return ws.ssClient.Load().(pbs.SessionServiceClient).LookupSession(ctx, req)
return ws.ssClient.LookupSession(ctx, req)
}
func (ws *workerProxyServiceServer) CancelSession(ctx context.Context, req *pbs.CancelSessionRequest) (*pbs.CancelSessionResponse, error) {
return ws.ssClient.Load().(pbs.SessionServiceClient).CancelSession(ctx, req)
return ws.ssClient.CancelSession(ctx, req)
}
func (ws *workerProxyServiceServer) ActivateSession(ctx context.Context, req *pbs.ActivateSessionRequest) (*pbs.ActivateSessionResponse, error) {
return ws.ssClient.Load().(pbs.SessionServiceClient).ActivateSession(ctx, req)
return ws.ssClient.ActivateSession(ctx, req)
}
func (ws *workerProxyServiceServer) AuthorizeConnection(ctx context.Context, req *pbs.AuthorizeConnectionRequest) (*pbs.AuthorizeConnectionResponse, error) {
return ws.ssClient.Load().(pbs.SessionServiceClient).AuthorizeConnection(ctx, req)
return ws.ssClient.AuthorizeConnection(ctx, req)
}
func (ws *workerProxyServiceServer) ConnectConnection(ctx context.Context, req *pbs.ConnectConnectionRequest) (*pbs.ConnectConnectionResponse, error) {
return ws.ssClient.Load().(pbs.SessionServiceClient).ConnectConnection(ctx, req)
return ws.ssClient.ConnectConnection(ctx, req)
}
func (ws *workerProxyServiceServer) CloseConnection(ctx context.Context, req *pbs.CloseConnectionRequest) (*pbs.CloseConnectionResponse, error) {
return ws.ssClient.Load().(pbs.SessionServiceClient).CloseConnection(ctx, req)
return ws.ssClient.CloseConnection(ctx, req)
}
@@ -6,36 +6,60 @@ import (
"google.golang.org/grpc"
)
// TODO (lcr 08/2021): implement mockSessionServiceClient outside of primitive support for
// ConnectConnection
type mockSessionServiceClient struct{}
type mockSessionServiceClient struct {
LookupSessionFn func(context.Context, *LookupSessionRequest) (*LookupSessionResponse, error)
ActivateSessionFn func(context.Context, *ActivateSessionRequest) (*ActivateSessionResponse, error)
CancelSessionFn func(context.Context, *CancelSessionRequest) (*CancelSessionResponse, error)
AuthorizeConnectionFn func(context.Context, *AuthorizeConnectionRequest) (*AuthorizeConnectionResponse, error)
ConnectConnectionFn func(context.Context, *ConnectConnectionRequest) (*ConnectConnectionResponse, error)
CloseConnectionFn func(context.Context, *CloseConnectionRequest) (*CloseConnectionResponse, error)
}
func NewMockSessionServiceClient() SessionServiceClient {
// NewMockSessionServiceClient returns a mock SessionServiceClient which allows
// the mocking out specific calls to a SessionService by assigning values to
// the respective mock client member variables.
func NewMockSessionServiceClient() *mockSessionServiceClient {
return &mockSessionServiceClient{}
}
func (c *mockSessionServiceClient) LookupSession(_ context.Context, _ *LookupSessionRequest, _ ...grpc.CallOption) (*LookupSessionResponse, error) {
func (c *mockSessionServiceClient) LookupSession(ctx context.Context, req *LookupSessionRequest, _ ...grpc.CallOption) (*LookupSessionResponse, error) {
if c.LookupSessionFn != nil {
return c.LookupSessionFn(ctx, req)
}
panic("not implemented")
}
func (c *mockSessionServiceClient) ActivateSession(_ context.Context, _ *ActivateSessionRequest, _ ...grpc.CallOption) (*ActivateSessionResponse, error) {
func (c *mockSessionServiceClient) ActivateSession(ctx context.Context, req *ActivateSessionRequest, _ ...grpc.CallOption) (*ActivateSessionResponse, error) {
if c.ActivateSessionFn != nil {
return c.ActivateSessionFn(ctx, req)
}
panic("not implemented")
}
func (c *mockSessionServiceClient) CancelSession(_ context.Context, _ *CancelSessionRequest, _ ...grpc.CallOption) (*CancelSessionResponse, error) {
func (c *mockSessionServiceClient) CancelSession(ctx context.Context, req *CancelSessionRequest, _ ...grpc.CallOption) (*CancelSessionResponse, error) {
if c.CancelSessionFn != nil {
return c.CancelSessionFn(ctx, req)
}
panic("not implemented")
}
func (c *mockSessionServiceClient) AuthorizeConnection(_ context.Context, _ *AuthorizeConnectionRequest, _ ...grpc.CallOption) (*AuthorizeConnectionResponse, error) {
func (c *mockSessionServiceClient) AuthorizeConnection(ctx context.Context, req *AuthorizeConnectionRequest, _ ...grpc.CallOption) (*AuthorizeConnectionResponse, error) {
if c.AuthorizeConnectionFn != nil {
return c.AuthorizeConnectionFn(ctx, req)
}
panic("not implemented")
}
func (c *mockSessionServiceClient) ConnectConnection(_ context.Context, _ *ConnectConnectionRequest, _ ...grpc.CallOption) (*ConnectConnectionResponse, error) {
return &ConnectConnectionResponse{
Status: CONNECTIONSTATUS_CONNECTIONSTATUS_CONNECTED,
}, nil
}
func (c *mockSessionServiceClient) CloseConnection(_ context.Context, _ *CloseConnectionRequest, _ ...grpc.CallOption) (*CloseConnectionResponse, error) {
func (c *mockSessionServiceClient) ConnectConnection(ctx context.Context, req *ConnectConnectionRequest, _ ...grpc.CallOption) (*ConnectConnectionResponse, error) {
if c.ConnectConnectionFn != nil {
return c.ConnectConnectionFn(ctx, req)
}
panic("not implemented")
}
func (c *mockSessionServiceClient) CloseConnection(ctx context.Context, req *CloseConnectionRequest, _ ...grpc.CallOption) (*CloseConnectionResponse, error) {
if c.CloseConnectionFn != nil {
return c.CloseConnectionFn(ctx, req)
}
panic("not implemented")
}