refactor(worker): Move tcp_proxy into its own package (#1458)

* refactor(worker): Move tcp_proxy into its own package

This refactors:

- the tcp_proxy.go into worker/proxy/tcp
- add a missing unit test for the HandleTcpProxyV1
- refactors session.go into worker/session, needed to avoid import cycle
- adds a baseline for a mock controller session client
- go lint fixes

* Add sleep between spinning listener goroutine and dialing

Co-authored-by: Jeff Mitchell <jeffrey.mitchell@gmail.com>
This commit is contained in:
Louis RuchandJeff Mitchell authored and GitHub committed 2021-08-18 15:30:55 +02:00
1 parent c79daab68c
commit 31af49e76d
29 files changed
+1229 -773

No files matched your search

@@ -25,7 +25,7 @@ type SessionServiceClient interface {
ActivateSession(ctx context.Context, in *ActivateSessionRequest, opts ...grpc.CallOption) (*ActivateSessionResponse, error)
// CancelSession allows a worker to request that the controller cancel a session.
CancelSession(ctx context.Context, in *CancelSessionRequest, opts ...grpc.CallOption) (*CancelSessionResponse, error)
// AuthorizeConnection allows a worker to activate a session on a controller.
// AuthorizeConnection allows a worker to authorize a connection on a controller.
AuthorizeConnection(ctx context.Context, in *AuthorizeConnectionRequest, opts ...grpc.CallOption) (*AuthorizeConnectionResponse, error)
// ConnectConnection updates a connection to set it to connected
ConnectConnection(ctx context.Context, in *ConnectConnectionRequest, opts ...grpc.CallOption) (*ConnectConnectionResponse, error)
@@ -106,7 +106,7 @@ type SessionServiceServer interface {
ActivateSession(context.Context, *ActivateSessionRequest) (*ActivateSessionResponse, error)
// CancelSession allows a worker to request that the controller cancel a session.
CancelSession(context.Context, *CancelSessionRequest) (*CancelSessionResponse, error)
// AuthorizeConnection allows a worker to activate a session on a controller.
// AuthorizeConnection allows a worker to authorize a connection on a controller.
AuthorizeConnection(context.Context, *AuthorizeConnectionRequest) (*AuthorizeConnectionResponse, error)
// ConnectConnection updates a connection to set it to connected
ConnectConnection(context.Context, *ConnectConnectionRequest) (*ConnectConnectionResponse, error)
@@ -0,0 +1,41 @@
package services
import (
"context"
"google.golang.org/grpc"
)
// TODO (lcr 08/2021): implement mockSessionServiceClient outside of primitive support for
// ConnectConnection
type mockSessionServiceClient struct{}
func NewMockSessionServiceClient() SessionServiceClient {
return &mockSessionServiceClient{}
}
func (c *mockSessionServiceClient) LookupSession(_ context.Context, _ *LookupSessionRequest, _ ...grpc.CallOption) (*LookupSessionResponse, error) {
panic("not implemented")
}
func (c *mockSessionServiceClient) ActivateSession(_ context.Context, _ *ActivateSessionRequest, _ ...grpc.CallOption) (*ActivateSessionResponse, error) {
panic("not implemented")
}
func (c *mockSessionServiceClient) CancelSession(_ context.Context, _ *CancelSessionRequest, _ ...grpc.CallOption) (*CancelSessionResponse, error) {
panic("not implemented")
}
func (c *mockSessionServiceClient) AuthorizeConnection(_ context.Context, _ *AuthorizeConnectionRequest, _ ...grpc.CallOption) (*AuthorizeConnectionResponse, error) {
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) {
panic("not implemented")
}
@@ -19,7 +19,7 @@ service SessionService {
// CancelSession allows a worker to request that the controller cancel a session.
rpc CancelSession(CancelSessionRequest) returns (CancelSessionResponse) {}
// AuthorizeConnection allows a worker to activate a session on a controller.
// AuthorizeConnection allows a worker to authorize a connection on a controller.
rpc AuthorizeConnection(AuthorizeConnectionRequest) returns (AuthorizeConnectionResponse) {}
// ConnectConnection updates a connection to set it to connected
@@ -1017,9 +1017,9 @@ HostSourceIterationLoop:
for _, l := range libs {
reqs = append(reqs, credential.Request{
SourceId: l.Id(),
Purpose: credential.Purpose(l.CredentialPurpose()),
Purpose: l.CredentialPurpose(),
})
dynCreds = append(dynCreds, session.NewDynamicCredential(l.Id(), credential.Purpose(l.CredentialPurpose())))
dynCreds = append(dynCreds, session.NewDynamicCredential(l.Id(), l.CredentialPurpose()))
}
expTime := timestamppb.Now()
@@ -367,8 +367,7 @@ func (ws *workerServiceServer) ActivateSession(ctx context.Context, req *pbs.Act
}
func (ws *workerServiceServer) AuthorizeConnection(ctx context.Context, req *pbs.AuthorizeConnectionRequest) (*pbs.AuthorizeConnectionResponse, error) {
const op = "workers.(workerServiceServer"
const op = "workers.(workerServiceServer).AuthorizeConnection"
sessRepo, err := ws.sessionRepoFn()
if err != nil {
return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err)
@@ -399,7 +398,6 @@ func (ws *workerServiceServer) AuthorizeConnection(ctx context.Context, req *pbs
func (ws *workerServiceServer) ConnectConnection(ctx context.Context, req *pbs.ConnectConnectionRequest) (*pbs.ConnectConnectionResponse, error) {
const op = "workers.(workerServiceServer).ConnectConnection"
sessRepo, err := ws.sessionRepoFn()
if err != nil {
return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err)
@@ -419,25 +417,9 @@ func (ws *workerServiceServer) ConnectConnection(ctx context.Context, req *pbs.C
return nil, status.Error(codes.Internal, "Invalid connect connection response.")
}
ret := &pbs.ConnectConnectionResponse{
return &pbs.ConnectConnectionResponse{
Status: connStates[0].Status.ProtoVal(),
}
loggerPairs := []interface{}{
"session_id", connectionInfo.SessionId,
"connection_id", req.ConnectionId,
"client_tcp_address", req.ClientTcpAddress,
"client_tcp_port", req.ClientTcpPort,
}
switch req.GetType() {
case "tcp":
loggerPairs = append(loggerPairs,
"endpoint_tcp_address", connectionInfo.EndpointTcpAddress,
"endpoint_tcp_port", connectionInfo.EndpointTcpPort,
)
}
return ret, nil
}, nil
}
func (ws *workerServiceServer) CloseConnection(ctx context.Context, req *pbs.CloseConnectionRequest) (*pbs.CloseConnectionResponse, error) {
+17
View File
@@ -0,0 +1,17 @@
package common
import "time"
// In the future we could make this configurable
const (
// StatusInterval is the base duration used in the calculation of the random backoff
// during the worker status report
StatusInterval = 2 * time.Second
// StatusTimeout is the timeout duration on status calls to the controller from
// the worker
StatusTimeout = 5 * time.Second
)
// CredentialData represents the secret data of the credential
type CredentialData interface{}
+3
View File
@@ -0,0 +1,3 @@
// Package common contains types and helper functions that are used across the different packages under
// internal/server/worker.
package common
+1 -1
View File
@@ -10,6 +10,6 @@ type Config struct {
// Workers
*base.Server
// The underlying configuration, passed in here to avoid duplicating values
// everwyehere
// everywhere
RawConfig *config.Config
}
@@ -59,7 +59,7 @@ func (w *Worker) startControllerConnections() error {
return nil
}
func (w Worker) controllerDialerFunc() func(context.Context, string) (net.Conn, error) {
func (w *Worker) controllerDialerFunc() func(context.Context, string) (net.Conn, error) {
const op = "worker.(Worker).controllerDialerFunc"
return func(ctx context.Context, addr string) (net.Conn, error) {
tlsConf, authInfo, err := w.workerAuthTLSConfig()
@@ -130,11 +130,11 @@ func (w *Worker) createClientConn(addr string) error {
w.controllerStatusConn.Store(pbs.NewServerCoordinationServiceClient(cc))
w.controllerSessionConn.Store(pbs.NewSessionServiceClient(cc))
event.WriteSysEvent(context.TODO(), op, "connected to controller", "address", addr)
event.WriteSysEvent(w.baseContext, op, "connected to controller", "address", addr)
return nil
}
func (w Worker) workerAuthTLSConfig() (*tls.Config, *base.WorkerAuthInfo, error) {
func (w *Worker) workerAuthTLSConfig() (*tls.Config, *base.WorkerAuthInfo, error) {
var err error
info := &base.WorkerAuthInfo{
Name: w.conf.RawConfig.Worker.Name,
+87 -129
View File
@@ -11,6 +11,8 @@ import (
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/boundary/internal/proxy"
proxyHandlers "github.com/hashicorp/boundary/internal/servers/worker/proxy"
"github.com/hashicorp/boundary/internal/servers/worker/session"
"github.com/hashicorp/go-secure-stdlib/listenerutil"
"nhooyr.io/websocket"
"nhooyr.io/websocket/wspb"
@@ -20,7 +22,7 @@ type HandlerProperties struct {
ListenerConfig *listenerutil.ListenerConfig
}
// Handler returns an http.Handler for the API. This can be used on
// 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 {
// Create the muxer to handle the actual endpoints
@@ -35,7 +37,7 @@ func (w *Worker) handler(props HandlerProperties) http.Handler {
func (w *Worker) handleProxy() http.HandlerFunc {
const op = "worker.(Worker).handleProxy"
return http.HandlerFunc(func(wr http.ResponseWriter, r *http.Request) {
return func(wr http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if r.TLS == nil {
event.WriteError(ctx, op, errors.New("no request TLS information found"))
@@ -67,14 +69,14 @@ func (w *Worker) handleProxy() http.HandlerFunc {
wr.WriteHeader(http.StatusInternalServerError)
return
}
si := siRaw.(*sessionInfo)
si := siRaw.(*session.Info)
si.RLock()
expiration := si.lookupSessionResponse.GetExpiration()
tofuToken := si.lookupSessionResponse.GetTofuToken()
version := si.lookupSessionResponse.GetVersion()
endpoint := si.lookupSessionResponse.GetEndpoint()
// userId := si.lookupSessionResponse.GetAuthorization()
sessStatus := si.status
expiration := si.LookupSessionResponse.GetExpiration()
tofuToken := si.LookupSessionResponse.GetTofuToken()
version := si.LookupSessionResponse.GetVersion()
endpoint := si.LookupSessionResponse.GetEndpoint()
// userId := si.LookupSessionResponse.GetAuthorization()
sessStatus := si.Status
si.RUnlock()
opts := &websocket.AcceptOptions{
@@ -89,73 +91,106 @@ func (w *Worker) handleProxy() http.HandlerFunc {
// Later calls will cause this to noop if they return a different status
defer conn.Close(websocket.StatusNormalClosure, "done")
connCtx, connCancel := context.WithDeadline(r.Context(), expiration.AsTime())
connCtx, connCancel := context.WithDeadline(ctx, expiration.AsTime())
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
}
workerId := w.conf.RawConfig.Worker.Name
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"))
conn.Close(websocket.StatusPolicyViolation, "invalid handshake received")
if err = conn.Close(websocket.StatusPolicyViolation, "invalid handshake received"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
return
}
if len(handshake.GetTofuToken()) < 20 {
event.WriteError(ctx, op, errors.New("invalid tofu token"))
conn.Close(websocket.StatusUnsupportedData, "invalid tofu token")
if err = conn.Close(websocket.StatusUnsupportedData, "invalid tofu token"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
return
}
if tofuToken != "" {
if tofuToken != handshake.GetTofuToken() {
event.WriteError(ctx, op, errors.New("WARNING: mismatched tofu token"), event.WithInfo("session_id", sessionId))
conn.Close(websocket.StatusPolicyViolation, "tofu token not allowed")
if err = conn.Close(websocket.StatusPolicyViolation, "tofu token not allowed"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
return
}
} else {
if sessStatus != pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING {
event.WriteError(ctx, op, err, event.WithInfoMsg("no tofu token but not in correct session state"))
conn.Close(websocket.StatusInternalError, "refusing to activate session")
if err = conn.Close(websocket.StatusInternalError, "refusing to activate session"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
return
}
if handshake.Command == proxy.HANDSHAKECOMMAND_HANDSHAKECOMMAND_UNSPECIFIED {
sessStatus, err = w.activateSession(r.Context(), sessionId, handshake.GetTofuToken(), version)
sessStatus, err = session.Activate(ctx, sessClient, workerId, sessionId, handshake.GetTofuToken(), version)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("unable to validate session"))
conn.Close(websocket.StatusInternalError, "unable to activate session")
if err = conn.Close(websocket.StatusInternalError, "unable to activate session"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
return
}
}
}
if handshake.Command == proxy.HANDSHAKECOMMAND_HANDSHAKECOMMAND_SESSION_CANCEL {
_, err := w.cancelSession(r.Context(), sessionId)
_, err := session.Cancel(ctx, sessClient, sessionId)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("unable to cancel session"))
conn.Close(websocket.StatusInternalError, "unable to cancel session")
if err = conn.Close(websocket.StatusInternalError, "unable to cancel session"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
return
}
conn.Close(websocket.StatusNormalClosure, "session canceled")
if err = conn.Close(websocket.StatusNormalClosure, "session canceled"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
return
}
var ci *connInfo
// Verify the subprotocol has a supported proxy before calling AuthorizeConnection
handleProxyFn, err := proxyHandlers.GetHandler(conn.Subprotocol())
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("worker received request for unsupported protocol %s", conn.Subprotocol()))
if err = conn.Close(websocket.StatusProtocolError, "unsupported-protocol"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
return
}
var ci *session.ConnInfo
var connsLeft int32
ci, connsLeft, err = w.authorizeConnection(r.Context(), sessionId)
ci, connsLeft, err = session.AuthorizeConnection(ctx, sessClient, workerId, sessionId)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("unable to authorize connection"))
conn.Close(websocket.StatusInternalError, "unable to authorize connection")
if err = conn.Close(websocket.StatusInternalError, "unable to authorize connection"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
return
}
defer func() {
w.closeConnections(r.Context(), map[string]string{ci.id: si.id})
}()
defer session.CloseConnections(ctx, sessClient, w.sessionInfoMap, map[string]string{ci.Id: si.Id})
si.Lock()
ci.connCtx = connCtx
ci.connCancel = connCancel
si.connInfoMap[ci.id] = ci
si.status = sessStatus
connectionLimit := si.lookupSessionResponse.GetConnectionLimit()
ci.ConnCtx = connCtx
ci.ConnCancel = connCancel
si.ConnInfoMap[ci.Id] = ci
si.Status = sessStatus
connectionLimit := si.LookupSessionResponse.GetConnectionLimit()
si.Unlock()
handshakeResult := &proxy.HandshakeResult{
@@ -165,114 +200,37 @@ func (w *Worker) handleProxy() http.HandlerFunc {
}
if err := wspb.Write(connCtx, conn, handshakeResult); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error sending handshake result to client"))
conn.Close(websocket.StatusProtocolError, "unable to send handshake result")
if err = conn.Close(websocket.StatusProtocolError, "unable to send handshake result"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
return
}
switch conn.Subprotocol() {
case globals.TcpProxyV1:
w.handleTcpProxyV1(connCtx, clientAddr, conn, si, ci.id, endpoint)
default:
conn.Close(websocket.StatusProtocolError, "unsupported-protocol")
conf := proxyHandlers.Config{
ClientAddress: clientAddr,
ClientConn: conn,
RemoteEndpoint: endpoint,
SessionClient: sessClient,
SessionInfo: si,
ConnectionId: ci.Id,
}
if err := conf.Validate(); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error validating proxy config"))
if err = conn.Close(websocket.StatusInternalError, "unable to validate proxy parameters"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
return
}
})
handleProxyFn(connCtx, conf)
}
}
func (w *Worker) wrapGenericHandler(h http.Handler, props HandlerProperties) http.Handler {
func (w *Worker) wrapGenericHandler(h http.Handler, _ HandlerProperties) http.Handler {
return http.HandlerFunc(func(wr http.ResponseWriter, r *http.Request) {
// Set the Cache-Control header for all responses returned
wr.Header().Set("Cache-Control", "no-store")
h.ServeHTTP(wr, r)
})
}
/*
func WrapForwardedForHandler(h http.Handler, authorizedAddrs []*sockaddr.SockAddrMarshaler, rejectNotPresent, rejectNonAuthz bool, hopSkips int) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
headers, headersOK := r.Header[textproto.CanonicalMIMEHeaderKey("X-Forwarded-For")]
if !headersOK || len(headers) == 0 {
if !rejectNotPresent {
h.ServeHTTP(w, r)
return
}
respondError(w, http.StatusBadRequest, fmt.Errorf("missing x-forwarded-for header and configured to reject when not present"))
return
}
host, port, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
// If not rejecting treat it like we just don't have a valid
// header because we can't do a comparison against an address we
// can't understand
if !rejectNotPresent {
h.ServeHTTP(w, r)
return
}
respondError(w, http.StatusBadRequest, errwrap.Wrapf("error parsing client hostport: {{err}}", err))
return
}
addr, err := sockaddr.NewIPAddr(host)
if err != nil {
// We treat this the same as the case above
if !rejectNotPresent {
h.ServeHTTP(w, r)
return
}
respondError(w, http.StatusBadRequest, errwrap.Wrapf("error parsing client address: {{err}}", err))
return
}
var found bool
for _, authz := range authorizedAddrs {
if authz.Contains(addr) {
found = true
break
}
}
if !found {
// If we didn't find it and aren't configured to reject, simply
// don't trust it
if !rejectNonAuthz {
h.ServeHTTP(w, r)
return
}
respondError(w, http.StatusBadRequest, fmt.Errorf("client address not authorized for x-forwarded-for and configured to reject connection"))
return
}
// At this point we have at least one value and it's authorized
// Split comma separated ones, which are common. This brings it in line
// to the multiple-header case.
var acc []string
for _, header := range headers {
vals := strings.Split(header, ",")
for _, v := range vals {
acc = append(acc, strings.TrimSpace(v))
}
}
indexToUse := len(acc) - 1 - hopSkips
if indexToUse < 0 {
// This is likely an error in either configuration or other
// infrastructure. We could either deny the request, or we
// could simply not trust the value. Denying the request is
// "safer" since if this logic is configured at all there may
// be an assumption it can always be trusted. Given that we can
// deny accepting the request at all if it's not from an
// authorized address, if we're at this point the address is
// authorized (or we've turned off explicit rejection) and we
// should assume that what comes in should be properly
// formatted.
respondError(w, http.StatusBadRequest, fmt.Errorf("malformed x-forwarded-for configuration or request, hops to skip (%d) would skip before earliest chain link (chain length %d)", hopSkips, len(headers)))
return
}
r.RemoteAddr = net.JoinHostPort(acc[indexToUse], port)
h.ServeHTTP(w, r)
return
})
}
*/
+2 -3
View File
@@ -18,13 +18,12 @@ import (
func (w *Worker) startListeners() error {
const op = "worker.(Worker).startListeners"
ctx := context.TODO()
servers := make([]func(), 0, len(w.conf.Listeners))
e := event.SysEventer()
if e == nil {
return fmt.Errorf("%s: sys eventer not initialized", op)
}
logger, err := e.StandardLogger(ctx, "listeners", event.ErrorType)
logger, err := e.StandardLogger(w.baseContext, "listeners", event.ErrorType)
if err != nil {
return fmt.Errorf("%s: unable to initialize std logger: %w", op, err)
}
@@ -110,7 +109,7 @@ func (w *Worker) stopListeners() error {
defer shutdownKillCancel()
if localLn.HTTPServer != nil {
localLn.HTTPServer.Shutdown(shutdownKill)
_ = localLn.HTTPServer.Shutdown(shutdownKill)
}
}()
}
@@ -0,0 +1,5 @@
package worker
import (
_ "github.com/hashicorp/boundary/internal/servers/worker/proxy/tcp"
)
+4
View File
@@ -0,0 +1,4 @@
// Package proxy contains a collection of proxy handlers for the worker to call once a
// connection has been authorized. Each proxy handler should mark the connection as
// connected once the proxy has successfully been established.
package proxy
+11
View File
@@ -0,0 +1,11 @@
package proxy
// Option - how Options are passed as arguments.
type Option func(*options)
// options = how options are represented
type options struct{}
func getDefaultOptions() options {
return options{}
}
+81
View File
@@ -0,0 +1,81 @@
package proxy
import (
"context"
"errors"
"net"
"sync"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/internal/servers/worker/session"
"nhooyr.io/websocket"
)
// Config provides the core parameters needed for a worker to create a proxy between
// a provided ClientConn and the RemoteEndpoint, as well as the parameters to update
// the connection in the connection repository.
type Config struct {
ClientAddress *net.TCPAddr
ClientConn *websocket.Conn
RemoteEndpoint string
SessionClient pbs.SessionServiceClient
SessionInfo *session.Info
ConnectionId string
}
// Validate checks that the provided config is valid. If invalid, an error is returned
// specifying the error.
func (c Config) Validate() error {
switch {
case c.ClientAddress == nil:
return errors.New("missing client address")
case c.ClientConn == nil:
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.ConnectionId == "":
return errors.New("missing connection id")
default:
return nil
}
}
// Handler is the type that all proxies need to implement to be called by the worker
// when a new client connection is created.
type Handler func(ctx context.Context, config Config, opt ...Option)
var (
// handlers is the map of registered handlers
handlers sync.Map
// ErrUnknownProtocol specifies the provided protocol has no registered handler
ErrUnknownProtocol = errors.New("proxy: handler not found for protocol")
// ErrProtocolAlreadyRegistered specifies the provided protocol has already been registered
ErrProtocolAlreadyRegistered = errors.New("proxy: protocol already registered")
)
// RegisterHandler registers the handler to call for the protocol. The protocol is
// negotiated when the connection was established to the worker.
func RegisterHandler(protocol string, handler Handler) error {
_, loaded := handlers.LoadOrStore(protocol, handler)
if loaded {
return ErrProtocolAlreadyRegistered
}
return nil
}
// GetHandler returns the handler registered for the provided protocol. If the protocol
// is not registered nil and ErrUnknownProtocol is returned.
func GetHandler(protocol string) (Handler, error) {
handler, ok := handlers.Load(protocol)
if !ok {
return nil, ErrUnknownProtocol
}
return handler.(Handler), nil
}
+170
View File
@@ -0,0 +1,170 @@
package proxy
import (
"context"
"net"
"testing"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/internal/servers/worker/session"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"nhooyr.io/websocket"
)
func TestConfigValidate(t *testing.T) {
t.Parallel()
// Create mock data for session management section of HandleTcpProxyV1
clientAddr := &net.TCPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 50000,
}
sessClient := pbs.NewMockSessionServiceClient()
si := &session.Info{
Id: "one",
}
conn := &websocket.Conn{}
tests := []struct {
name string
conf Config
wantErr bool
wantErrMsg string
}{
{
name: "missing-client-address",
conf: Config{
ClientConn: conn,
RemoteEndpoint: "tcp://remote",
SessionClient: sessClient,
SessionInfo: si,
ConnectionId: "connection-id",
},
wantErr: true,
wantErrMsg: "missing client address",
},
{
name: "missing-client-conn",
conf: Config{
ClientAddress: clientAddr,
RemoteEndpoint: "tcp://remote",
SessionClient: sessClient,
SessionInfo: si,
ConnectionId: "connection-id",
},
wantErr: true,
wantErrMsg: "missing client connection",
},
{
name: "missing-remote-endpoint",
conf: Config{
ClientAddress: clientAddr,
ClientConn: conn,
SessionClient: sessClient,
SessionInfo: si,
ConnectionId: "connection-id",
},
wantErr: true,
wantErrMsg: "missing remote endpoint",
},
{
name: "missing-session-client",
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",
},
{
name: "missing-connection-id",
conf: Config{
ClientAddress: clientAddr,
ClientConn: conn,
RemoteEndpoint: "tcp://remote",
SessionClient: sessClient,
SessionInfo: si,
},
wantErr: true,
wantErrMsg: "missing connection id",
},
{
name: "valid",
conf: Config{
ClientAddress: clientAddr,
ClientConn: conn,
RemoteEndpoint: "tcp://remote",
SessionClient: sessClient,
SessionInfo: si,
ConnectionId: "connection-id",
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
err := tt.conf.Validate()
if tt.wantErr {
require.Error(err)
assert.Equal(tt.wantErrMsg, err.Error())
return
}
require.NoError(err)
})
}
}
func TestRegisterHandler(t *testing.T) {
t.Parallel()
assert, require := assert.New(t), require.New(t)
fn := func(context.Context, Config, ...Option) { return }
err := RegisterHandler("protocol", fn)
require.NoError(err)
// Register function with same protocol
err = RegisterHandler("protocol", fn)
require.Error(err)
assert.ErrorIs(err, ErrProtocolAlreadyRegistered)
err = RegisterHandler("new-protocol", fn)
require.NoError(err)
}
func TestGetHandler(t *testing.T) {
t.Parallel()
assert, require := assert.New(t), require.New(t)
fn := func(context.Context, Config, ...Option) {}
err := RegisterHandler("fn", fn)
require.NoError(err)
gotFn, err := GetHandler("fake")
require.Error(err)
assert.ErrorIs(err, ErrUnknownProtocol)
assert.Nil(gotFn)
gotFn, err = GetHandler("fn")
require.NoError(err)
assert.NotNil(gotFn)
}
+105
View File
@@ -0,0 +1,105 @@
package tcp
import (
"context"
"io"
"net"
"net/url"
"sync"
"github.com/hashicorp/boundary/globals"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/boundary/internal/servers/worker/proxy"
"github.com/hashicorp/boundary/internal/servers/worker/session"
"nhooyr.io/websocket"
)
func init() {
err := proxy.RegisterHandler(globals.TcpProxyV1, handleTcpProxyV1)
if err != nil {
panic(err)
}
}
// handleTcpProxyV1 creates a tcp proxy between the incoming websocket conn and the
// connection it creates with the remote endpoint. handleTcpProxyV1 sets the connectionId
// as connected in the repository.
//
// handleTcpProxyV1 blocks until an error (EOF on happy path) is received on either
// connection.
func handleTcpProxyV1(ctx context.Context, conf proxy.Config, _ ...proxy.Option) {
const op = "tcp.HandleTcpProxyV1"
si := conf.SessionInfo
si.RLock()
sessionId := si.LookupSessionResponse.GetAuthorization().GetSessionId()
si.RUnlock()
conn := conf.ClientConn
sessionUrl, err := url.Parse(conf.RemoteEndpoint)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error parsing endpoint information", "session_id", sessionId, "endpoint", conf.RemoteEndpoint))
if err = conn.Close(websocket.StatusInternalError, "cannot parse endpoint url"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
return
}
if sessionUrl.Scheme != "tcp" {
event.WriteError(ctx, op, err, event.WithInfo("session_id", sessionId, "endpoint", conf.RemoteEndpoint))
if err = conn.Close(websocket.StatusInternalError, "invalid scheme for type"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
return
}
remoteConn, err := net.Dial("tcp", sessionUrl.Host)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error dialing endpoint", "endpoint", conf.RemoteEndpoint))
if err = conn.Close(websocket.StatusInternalError, "endpoint dialing failed"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
return
}
// Assert this for better Go 1.11 splice support
tcpRemoteConn := remoteConn.(*net.TCPConn)
endpointAddr := tcpRemoteConn.RemoteAddr().(*net.TCPAddr)
connectionInfo := &pbs.ConnectConnectionRequest{
ConnectionId: conf.ConnectionId,
ClientTcpAddress: conf.ClientAddress.IP.String(),
ClientTcpPort: uint32(conf.ClientAddress.Port),
EndpointTcpAddress: endpointAddr.IP.String(),
EndpointTcpPort: uint32(endpointAddr.Port),
Type: "tcp",
}
connStatus, err := session.ConnectConnection(ctx, conf.SessionClient, connectionInfo)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error marking connection as connected"))
if err = conn.Close(websocket.StatusInternalError, "failed to mark connection as connected"); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing client connection"))
}
return
}
si.Lock()
si.ConnInfoMap[conf.ConnectionId].Status = connStatus
si.Unlock()
// Get a wrapped net.Conn so we can use io.Copy
netConn := websocket.NetConn(ctx, conn, websocket.MessageBinary)
connWg := new(sync.WaitGroup)
connWg.Add(2)
go func() {
defer connWg.Done()
_, _ = io.Copy(netConn, tcpRemoteConn)
_ = netConn.Close()
_ = tcpRemoteConn.Close()
}()
go func() {
defer connWg.Done()
_, _ = io.Copy(tcpRemoteConn, netConn)
_ = tcpRemoteConn.Close()
_ = netConn.Close()
}()
connWg.Wait()
}
@@ -0,0 +1,100 @@
package tcp
import (
"context"
"fmt"
"net"
"testing"
"github.com/hashicorp/boundary/internal/gen/controller/api/resources/targets"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/internal/servers/worker/proxy"
"github.com/hashicorp/boundary/internal/servers/worker/session"
"github.com/hashicorp/boundary/sdk/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"nhooyr.io/websocket"
)
func TestHandleTcpProxyV1(t *testing.T) {
t.Parallel()
require, assert := require.New(t), assert.New(t)
ctx, cancelCtx := context.WithCancel(context.Background())
clientConn, proxyConn := proxy.TestWsConn(t, ctx)
require.NotNil(clientConn)
require.NotNil(proxyConn)
port := testutil.TestFreePort(t)
l, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
require.NoError(err)
defer l.Close()
var endpointConn net.Conn
ready := make(chan struct{})
go func() {
endpointConn, err = l.Accept()
require.NoError(err)
defer endpointConn.Close()
ready <- struct{}{}
// block waiting for test to complete
<-ctx.Done()
}()
// Create mock data for session management section of HandleTcpProxyV1
clientAddr := &net.TCPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 50000,
}
sessClient := pbs.NewMockSessionServiceClient()
si := &session.Info{
Id: "one",
LookupSessionResponse: &pbs.LookupSessionResponse{
Authorization: &targets.SessionAuthorizationData{
SessionId: "mock-session",
},
},
ConnInfoMap: map[string]*session.ConnInfo{
"mock-connection": {},
},
}
conf := proxy.Config{
ClientAddress: clientAddr,
ClientConn: proxyConn,
RemoteEndpoint: fmt.Sprintf("tcp://localhost:%d", port),
SessionClient: sessClient,
SessionInfo: si,
ConnectionId: "mock-connection",
}
go handleTcpProxyV1(ctx, conf)
// wait for HandleTcpProxyV1 to dial endpoint
<-ready
netConn := websocket.NetConn(ctx, clientConn, websocket.MessageBinary)
// Write from endpoint to client
writeLen, err := endpointConn.Write([]byte("endpoint write to client via proxy"))
require.NoError(err)
b := make([]byte, writeLen)
readLen, err := netConn.Read(b)
require.NoError(err)
assert.Equal(writeLen, readLen)
assert.Equal("endpoint write to client via proxy", string(b))
// Write from client to endpoint
writeLen, err = netConn.Write([]byte("client write to endpoint via proxy"))
require.NoError(err)
b1 := make([]byte, writeLen)
readLen, err = endpointConn.Read(b1)
require.NoError(err)
assert.Equal(writeLen, readLen)
assert.Equal("client write to endpoint via proxy", string(b1))
cancelCtx()
}
+50
View File
@@ -0,0 +1,50 @@
package proxy
import (
"context"
"fmt"
"net/http"
"sync"
"testing"
"time"
"github.com/hashicorp/boundary/sdk/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"nhooyr.io/websocket"
)
// TestWsConn creates a websocket client and handler simulating the local websocket proxy
// created during Boundary connect. The proxyConn returned should be used as the connection
// passed into the worker proxy handler, while the clientConn can be used to simulate
// the local end user connection.
func TestWsConn(t *testing.T, ctx context.Context) (clientConn, proxyConn *websocket.Conn) {
t.Helper()
require, assert := require.New(t), assert.New(t)
var wg sync.WaitGroup
wg.Add(1)
port := testutil.TestFreePort(t)
go func() {
err := http.ListenAndServe(fmt.Sprintf(":%d", port), http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
var err error
proxyConn, err = websocket.Accept(w, r, nil)
require.NoError(err)
wg.Done()
// block waiting for test to complete
select {
case <-ctx.Done():
}
}))
assert.NoError(err)
}()
time.Sleep(time.Duration(1.5 * float64(time.Second)))
clientConn, _, err := websocket.Dial(ctx, fmt.Sprintf("ws://localhost:%d", port), nil)
require.NoError(err)
wg.Wait()
return
}
@@ -0,0 +1,47 @@
package proxy
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"nhooyr.io/websocket"
)
func Test_TestWsConn(t *testing.T) {
t.Parallel()
require, assert := require.New(t), assert.New(t)
ctx, cancelCtx := context.WithCancel(context.Background())
clientConn, proxyConn := TestWsConn(t, ctx)
successfulRead := make(chan struct{})
go func() {
_, msg, err := proxyConn.Read(ctx)
require.NoError(err)
assert.Equal("client to proxy", string(msg))
successfulRead <- struct{}{}
}()
err := clientConn.Write(ctx, websocket.MessageBinary, []byte("client to proxy"))
require.NoError(err)
// Wait for read to verify success
<-successfulRead
go func() {
_, msg, err := clientConn.Read(ctx)
require.NoError(err)
assert.Equal("proxy to client", string(msg))
successfulRead <- struct{}{}
}()
err = proxyConn.Write(ctx, websocket.MessageBinary, []byte("proxy to client"))
require.NoError(err)
// Wait for read to verify success
<-successfulRead
cancelCtx()
}
-422
View File
@@ -1,422 +0,0 @@
package worker
import (
"context"
"crypto/ed25519"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"strings"
"sync"
"time"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/boundary/internal/session"
)
const validateSessionTimeout = 90 * time.Second
var errMakeSessionCloseInfoNilCloseInfo = errors.New("nil closeInfo supplied to makeSessionCloseInfo, this is a bug, please report it")
type connInfo struct {
id string
connCtx context.Context
connCancel context.CancelFunc
status pbs.CONNECTIONSTATUS
closeTime time.Time
}
type sessionInfo struct {
sync.RWMutex
id string
sessionTls *tls.Config
status pbs.SESSIONSTATUS
lookupSessionResponse *pbs.LookupSessionResponse
connInfoMap map[string]*connInfo
}
func (w *Worker) getSessionTls(hello *tls.ClientHelloInfo) (*tls.Config, error) {
const op = "worker.(Worker).getSessionTls"
ctx := context.TODO()
var sessionId string
switch {
case strings.HasPrefix(hello.ServerName, "s_"):
sessionId = hello.ServerName
default:
event.WriteSysEvent(ctx, op, "invalid session in SNI", "session_id", hello.ServerName)
return nil, fmt.Errorf("could not find session ID in SNI")
}
rawConn := w.controllerSessionConn.Load()
if rawConn == nil {
event.WriteSysEvent(ctx, op, "could not get a controller client", "session_id", sessionId)
return nil, errors.New("could not get a controller client")
}
conn, ok := rawConn.(pbs.SessionServiceClient)
if !ok {
event.WriteSysEvent(ctx, op, "could not cast controller client to the real thing", "session_id", sessionId)
return nil, errors.New("could not cast atomic controller client to the real thing")
}
if conn == nil {
event.WriteSysEvent(ctx, op, "controller client is nil", "session_id", sessionId)
return nil, errors.New("controller client is nil")
}
timeoutContext, cancel := context.WithTimeout(w.baseContext, validateSessionTimeout)
defer cancel()
resp, err := conn.LookupSession(timeoutContext, &pbs.LookupSessionRequest{
ServerId: w.conf.RawConfig.Worker.Name,
SessionId: sessionId,
})
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)
}
if len(parsedCert.DNSNames) != 1 {
return nil, fmt.Errorf("invalid length of DNS names (%d) in parsed certificate", len(parsedCert.DNSNames))
}
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,
},
},
ServerName: parsedCert.DNSNames[0],
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: certPool,
MinVersion: tls.VersionTLS13,
}
si := &sessionInfo{
id: resp.GetAuthorization().GetSessionId(),
sessionTls: tlsConf,
lookupSessionResponse: resp,
status: resp.GetStatus(),
connInfoMap: make(map[string]*connInfo),
}
// TODO: Periodicially 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.(*sessionInfo)
actualSi.Lock()
actualSi.lookupSessionResponse = resp
actualSi.Unlock()
}
return tlsConf, nil
}
func (w *Worker) activateSession(ctx context.Context, sessionId, tofuToken string, version uint32) (pbs.SESSIONSTATUS, error) {
rawConn := w.controllerSessionConn.Load()
if rawConn == nil {
return pbs.SESSIONSTATUS_SESSIONSTATUS_UNSPECIFIED, errors.New("could not get a controller client")
}
conn, ok := rawConn.(pbs.SessionServiceClient)
if !ok {
return pbs.SESSIONSTATUS_SESSIONSTATUS_UNSPECIFIED, errors.New("could not cast atomic controller client to the real thing")
}
if conn == nil {
return pbs.SESSIONSTATUS_SESSIONSTATUS_UNSPECIFIED, errors.New("controller client is nil")
}
resp, err := conn.ActivateSession(ctx, &pbs.ActivateSessionRequest{
SessionId: sessionId,
TofuToken: tofuToken,
Version: version,
WorkerId: w.conf.RawConfig.Worker.Name,
})
if err != nil {
return pbs.SESSIONSTATUS_SESSIONSTATUS_UNSPECIFIED, fmt.Errorf("error activating session: %w", err)
}
return resp.GetStatus(), nil
}
func (w *Worker) cancelSession(ctx context.Context, sessionId string) (pbs.SESSIONSTATUS, error) {
rawConn := w.controllerSessionConn.Load()
if rawConn == nil {
return pbs.SESSIONSTATUS_SESSIONSTATUS_UNSPECIFIED, errors.New("could not get a controller client")
}
conn, ok := rawConn.(pbs.SessionServiceClient)
if !ok {
return pbs.SESSIONSTATUS_SESSIONSTATUS_UNSPECIFIED, errors.New("could not cast atomic controller client to the real thing")
}
if conn == nil {
return pbs.SESSIONSTATUS_SESSIONSTATUS_UNSPECIFIED, errors.New("controller client is nil")
}
resp, err := conn.CancelSession(ctx, &pbs.CancelSessionRequest{
SessionId: sessionId,
})
if err != nil {
return pbs.SESSIONSTATUS_SESSIONSTATUS_UNSPECIFIED, fmt.Errorf("error canceling session: %w", err)
}
return resp.GetStatus(), nil
}
func (w *Worker) authorizeConnection(ctx context.Context, sessionId string) (*connInfo, int32, error) {
rawConn := w.controllerSessionConn.Load()
if rawConn == nil {
return nil, 0, errors.New("could not get a controller client")
}
conn, ok := rawConn.(pbs.SessionServiceClient)
if !ok {
return nil, 0, errors.New("could not cast atomic controller client to the real thing")
}
if conn == nil {
return nil, 0, errors.New("controller client is nil")
}
resp, err := conn.AuthorizeConnection(ctx, &pbs.AuthorizeConnectionRequest{
SessionId: sessionId,
WorkerId: w.conf.RawConfig.Worker.Name,
})
if err != nil {
return nil, 0, fmt.Errorf("error authorizing connection: %w", err)
}
return &connInfo{
id: resp.ConnectionId,
status: resp.GetStatus(),
}, resp.GetConnectionsLeft(), nil
}
func (w *Worker) connectConnection(ctx context.Context, req *pbs.ConnectConnectionRequest) (pbs.CONNECTIONSTATUS, error) {
rawConn := w.controllerSessionConn.Load()
if rawConn == nil {
return pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_UNSPECIFIED, errors.New("could not get a controller client")
}
conn, ok := rawConn.(pbs.SessionServiceClient)
if !ok {
return pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_UNSPECIFIED, errors.New("could not cast atomic controller client to the real thing")
}
if conn == nil {
return pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_UNSPECIFIED, errors.New("controller client is nil")
}
resp, err := conn.ConnectConnection(ctx, req)
if err != nil {
return pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_UNSPECIFIED, err
}
if resp.GetStatus() != pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CONNECTED {
return pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_UNSPECIFIED, fmt.Errorf("unexpected state returned: %v", resp.GetStatus().String())
}
return resp.GetStatus(), nil
}
func (w *Worker) closeConnection(ctx context.Context, req *pbs.CloseConnectionRequest) (*pbs.CloseConnectionResponse, error) {
const op = "worker.(Worker).closeConnections"
rawConn := w.controllerSessionConn.Load()
if rawConn == nil {
return nil, errors.New("could not get a controller client")
}
conn, ok := rawConn.(pbs.SessionServiceClient)
if !ok {
return nil, errors.New("could not cast atomic controller client to the real thing")
}
if conn == nil {
return nil, errors.New("controller client is nil")
}
resp, err := conn.CloseConnection(ctx, req)
if err != nil {
return nil, err
}
if len(resp.GetCloseResponseData()) != len(req.GetCloseRequestData()) {
event.WriteError(ctx, op, errors.New("mismatched number of states returned on connection closed"), event.WithInfo("expected", len(req.GetCloseRequestData()), "got", len(resp.GetCloseResponseData())))
}
return resp, nil
}
// closeConnections is a helper worker function that sends connection
// close requests to the controller, and sets close times within the
// worker. It is called during the worker status loop and on
// connection exit on the proxy.
//
// closeInfo is a map of connections mapped to their individual
// session.
func (w *Worker) closeConnections(ctx context.Context, closeInfo map[string]string) {
const op = "worker.(Worker).closeConnections"
if closeInfo == nil {
// This should not happen, but it's a no-op if it does. Just
// return.
return
}
// How we handle close info depends on whether or not we succeeded with
// marking them closed on the controller.
var sessionCloseInfo map[string][]*pbs.CloseConnectionResponseData
var err error
// TODO: This, along with the status call to the controller, probably needs a
// bit of formalization in terms of how we handle timeouts. For now, this
// just ensures consistency with the same status call in that it times out
// within an adequate period of time.
closeConnCtx, closeConnCancel := context.WithTimeout(ctx, statusTimeout)
defer closeConnCancel()
response, err := w.closeConnection(closeConnCtx, w.makeCloseConnectionRequest(closeInfo))
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error marking connections closed",
"warning", "error contacting controller, connections will be closed only on worker",
"session_and_connection_ids", fmt.Sprintf("%#v", closeInfo),
))
// Since we could not reach the controller, we have to make a "fake" response set.
sessionCloseInfo, err = w.makeFakeSessionCloseInfo(closeInfo)
} else {
// Connection succeeded, so we can proceed with making the sessionCloseInfo
// off of the response data.
sessionCloseInfo, err = w.makeSessionCloseInfo(closeInfo, response)
}
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("serious error in processing return data from controller, aborting additional session/connection state modification"))
return
}
// Mark connections as closed
_, errs := w.setCloseTimeForResponse(sessionCloseInfo)
if len(errs) > 0 {
for _, err := range errs {
event.WriteError(ctx, op, err, event.WithInfoMsg("error marking connection closed in state"))
}
}
}
// makeCloseConnectionRequest creates a CloseConnectionRequest for
// use with closing connections.
//
// closeInfo is a map, indexed by connection ID, to the individual
// sessions IDs that those connections belong to. The values are
// ignored; the parameter is expected as such just for convenience of
// its caller.
func (w *Worker) makeCloseConnectionRequest(closeInfo map[string]string) *pbs.CloseConnectionRequest {
closeData := make([]*pbs.CloseConnectionRequestData, 0, len(closeInfo))
for connId := range closeInfo {
closeData = append(closeData, &pbs.CloseConnectionRequestData{
ConnectionId: connId,
Reason: session.UnknownReason.String(),
})
}
return &pbs.CloseConnectionRequest{
CloseRequestData: closeData,
}
}
// makeSessionCloseInfo takes the response from CloseConnections and
// our original closeInfo map and makes a map of slices, indexed by
// session ID, of all of the connection responses. This allows us to
// easily lock on session once for all connections in
// setCloseTimeForResponse.
func (w *Worker) makeSessionCloseInfo(
closeInfo map[string]string,
response *pbs.CloseConnectionResponse,
) (map[string][]*pbs.CloseConnectionResponseData, error) {
if closeInfo == nil {
return nil, errMakeSessionCloseInfoNilCloseInfo
}
result := make(map[string][]*pbs.CloseConnectionResponseData)
for _, v := range response.GetCloseResponseData() {
result[closeInfo[v.GetConnectionId()]] = append(result[closeInfo[v.GetConnectionId()]], v)
}
return result, nil
}
// makeFakeSessionCloseInfo makes a "fake" makeFakeSessionCloseInfo, intended
// for use when we can't contact the controller.
func (w *Worker) makeFakeSessionCloseInfo(
closeInfo map[string]string,
) (map[string][]*pbs.CloseConnectionResponseData, error) {
if closeInfo == nil {
return nil, errMakeSessionCloseInfoNilCloseInfo
}
result := make(map[string][]*pbs.CloseConnectionResponseData)
for connectionId, sessionId := range closeInfo {
result[sessionId] = append(result[sessionId], &pbs.CloseConnectionResponseData{
ConnectionId: connectionId,
Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED,
})
}
return result, nil
}
// setCloseTimeForResponse iterates a CloseConnectionResponse and
// sets the close time for any connection found to be closed to the
// current time.
//
// sessionCloseInfo can be derived from the closeInfo supplied to
// makeCloseConnectionRequest through reverseCloseInfo, which creates
// a session ID to connection ID mapping.
//
// A non-zero error count does not necessarily mean the operation
// failed, as some connections may have been marked as closed. The
// actual list of connection IDs closed is returned as the first
// return value.
func (w *Worker) setCloseTimeForResponse(sessionCloseInfo map[string][]*pbs.CloseConnectionResponseData) ([]string, []error) {
closedIds := make([]string, 0)
var errors []error
for sessionId, responses := range sessionCloseInfo {
siRaw, ok := w.sessionInfoMap.Load(sessionId)
if !ok {
errors = append(errors, fmt.Errorf("could not find session ID %q in local state after closing connections", sessionId))
continue
}
si := siRaw.(*sessionInfo)
si.Lock()
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,
),
)
continue
}
ci.status = response.GetStatus()
if ci.status == pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED {
ci.closeTime = time.Now()
closedIds = append(closedIds, ci.id)
}
}
si.Unlock()
}
return closedIds, errors
}
+282
View File
@@ -0,0 +1,282 @@
package session
import (
"context"
"crypto/tls"
"errors"
"fmt"
"sync"
"time"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/boundary/internal/servers/worker/common"
"github.com/hashicorp/boundary/internal/session"
)
// ValidateSessionTimeout is the duration of the timeout when the worker queries the
// controller for the sessionId for which the connection is being requested.
const ValidateSessionTimeout = 90 * time.Second
var errMakeSessionCloseInfoNilCloseInfo = errors.New("nil closeInfo supplied to makeSessionCloseInfo, this is a bug, please report it")
// ConnInfo defines the information about a connection attached to a session
type ConnInfo struct {
Id string
ConnCtx context.Context
ConnCancel context.CancelFunc
Status pbs.CONNECTIONSTATUS
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
}
// Activate is a helper worker function that sends session activation request to the
// controller.
func Activate(ctx context.Context, sessClient pbs.SessionServiceClient, workerId, sessionId, tofuToken string, version uint32) (pbs.SESSIONSTATUS, error) {
resp, err := sessClient.ActivateSession(ctx, &pbs.ActivateSessionRequest{
SessionId: sessionId,
TofuToken: tofuToken,
Version: version,
WorkerId: workerId,
})
if err != nil {
return pbs.SESSIONSTATUS_SESSIONSTATUS_UNSPECIFIED, fmt.Errorf("error activating session: %w", err)
}
return resp.GetStatus(), nil
}
// Cancel is a helper worker function that sends session cancellation request to the
// controller.
func Cancel(ctx context.Context, sessClient pbs.SessionServiceClient, sessionId string) (pbs.SESSIONSTATUS, error) {
resp, err := sessClient.CancelSession(ctx, &pbs.CancelSessionRequest{
SessionId: sessionId,
})
if err != nil {
return pbs.SESSIONSTATUS_SESSIONSTATUS_UNSPECIFIED, fmt.Errorf("error canceling session: %w", err)
}
return resp.GetStatus(), nil
}
// 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) {
resp, err := sessClient.AuthorizeConnection(ctx, &pbs.AuthorizeConnectionRequest{
SessionId: sessionId,
WorkerId: workerId,
})
if err != nil {
return nil, 0, fmt.Errorf("error authorizing connection: %w", err)
}
return &ConnInfo{
Id: resp.ConnectionId,
Status: resp.GetStatus(),
}, resp.GetConnectionsLeft(), nil
}
// ConnectConnection is a helper worker function that sends connection
// connect request to the controller. It is called by the worker handler after a
// connection has been authorized.
func ConnectConnection(ctx context.Context, sessClient pbs.SessionServiceClient, req *pbs.ConnectConnectionRequest) (pbs.CONNECTIONSTATUS, error) {
resp, err := sessClient.ConnectConnection(ctx, req)
if err != nil {
return pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_UNSPECIFIED, err
}
if resp.GetStatus() != pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CONNECTED {
return pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_UNSPECIFIED, fmt.Errorf("unexpected state returned: %v", resp.GetStatus().String())
}
return resp.GetStatus(), nil
}
func closeConnection(ctx context.Context, sessClient pbs.SessionServiceClient, req *pbs.CloseConnectionRequest) (*pbs.CloseConnectionResponse, error) {
const op = "session.closeConnection"
resp, err := sessClient.CloseConnection(ctx, req)
if err != nil {
return nil, err
}
if len(resp.GetCloseResponseData()) != len(req.GetCloseRequestData()) {
event.WriteError(ctx, op, errors.New("mismatched number of states returned on connection closed"), event.WithInfo("expected", len(req.GetCloseRequestData()), "got", len(resp.GetCloseResponseData())))
}
return resp, nil
}
// CloseConnections is a helper worker function that sends connection
// close requests to the controller, and sets close times within the
// worker. It is called during the worker status loop and on
// connection exit on the proxy.
//
// 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) {
const op = "session.CloseConnections"
if closeInfo == nil {
// This should not happen, but it's a no-op if it does. Just
// return.
return
}
// How we handle close info depends on whether or not we succeeded with
// marking them closed on the controller.
var sessionCloseInfo map[string][]*pbs.CloseConnectionResponseData
var err error
// TODO: This, along with the status call to the controller, probably needs a
// bit of formalization in terms of how we handle timeouts. For now, this
// just ensures consistency with the same status call in that it times out
// within an adequate period of time.
closeConnCtx, closeConnCancel := context.WithTimeout(ctx, common.StatusTimeout)
defer closeConnCancel()
response, err := closeConnection(closeConnCtx, sessClient, makeCloseConnectionRequest(closeInfo))
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error marking connections closed",
"warning", "error contacting controller, connections will be closed only on worker",
"session_and_connection_ids", fmt.Sprintf("%#v", closeInfo),
))
// Since we could not reach the controller, we have to make a "fake" response set.
sessionCloseInfo, err = makeFakeSessionCloseInfo(closeInfo)
} else {
// Connection succeeded, so we can proceed with making the sessionCloseInfo
// off of the response data.
sessionCloseInfo, err = makeSessionCloseInfo(closeInfo, response)
}
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("serious error in processing return data from controller, aborting additional session/connection state modification"))
return
}
// Mark connections as closed
_, errs := setCloseTimeForResponse(sessionInfo, sessionCloseInfo)
if len(errs) > 0 {
for _, err := range errs {
event.WriteError(ctx, op, err, event.WithInfoMsg("error marking connection closed in state"))
}
}
}
// makeCloseConnectionRequest creates a CloseConnectionRequest for
// use with closing connections.
//
// closeInfo is a map, indexed by connection ID, to the individual
// sessions IDs that those connections belong to. The values are
// ignored; the parameter is expected as such just for convenience of
// its caller.
func makeCloseConnectionRequest(closeInfo map[string]string) *pbs.CloseConnectionRequest {
closeData := make([]*pbs.CloseConnectionRequestData, 0, len(closeInfo))
for connId := range closeInfo {
closeData = append(closeData, &pbs.CloseConnectionRequestData{
ConnectionId: connId,
Reason: session.UnknownReason.String(),
})
}
return &pbs.CloseConnectionRequest{
CloseRequestData: closeData,
}
}
// makeSessionCloseInfo takes the response from CloseConnections and
// our original closeInfo map and makes a map of slices, indexed by
// session ID, of all of the connection responses. This allows us to
// easily lock on session once for all connections in
// setCloseTimeForResponse.
func makeSessionCloseInfo(
closeInfo map[string]string,
response *pbs.CloseConnectionResponse,
) (map[string][]*pbs.CloseConnectionResponseData, error) {
if closeInfo == nil {
return nil, errMakeSessionCloseInfoNilCloseInfo
}
result := make(map[string][]*pbs.CloseConnectionResponseData)
for _, v := range response.GetCloseResponseData() {
result[closeInfo[v.GetConnectionId()]] = append(result[closeInfo[v.GetConnectionId()]], v)
}
return result, nil
}
// makeFakeSessionCloseInfo makes a "fake" makeFakeSessionCloseInfo, intended
// for use when we can't contact the controller.
func makeFakeSessionCloseInfo(
closeInfo map[string]string,
) (map[string][]*pbs.CloseConnectionResponseData, error) {
if closeInfo == nil {
return nil, errMakeSessionCloseInfoNilCloseInfo
}
result := make(map[string][]*pbs.CloseConnectionResponseData)
for connectionId, sessionId := range closeInfo {
result[sessionId] = append(result[sessionId], &pbs.CloseConnectionResponseData{
ConnectionId: connectionId,
Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED,
})
}
return result, nil
}
// setCloseTimeForResponse iterates a CloseConnectionResponse and
// sets the close time for any connection found to be closed to the
// current time.
//
// sessionCloseInfo can be derived from the closeInfo supplied to
// makeCloseConnectionRequest through reverseCloseInfo, which creates
// a session ID to connection ID mapping.
//
// A non-zero error count does not necessarily mean the operation
// failed, as some connections may have been marked as closed. The
// actual list of connection IDs closed is returned as the first
// return value.
func setCloseTimeForResponse(sessionInfo *sync.Map, sessionCloseInfo map[string][]*pbs.CloseConnectionResponseData) ([]string, []error) {
closedIds := make([]string, 0)
var errors []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))
continue
}
si := siRaw.(*Info)
si.Lock()
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,
),
)
continue
}
ci.Status = response.GetStatus()
if ci.Status == pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED {
ci.CloseTime = time.Now()
closedIds = append(closedIds, ci.Id)
}
}
si.Unlock()
}
return closedIds, errors
}
@@ -1,4 +1,4 @@
package worker
package session
import (
"errors"
@@ -20,7 +20,7 @@ func TestWorkerMakeCloseConnectionRequest(t *testing.T) {
{ConnectionId: "bar", Reason: session.UnknownReason.String()},
},
}
actual := new(Worker).makeCloseConnectionRequest(in)
actual := makeCloseConnectionRequest(in)
require.ElementsMatch(expected.GetCloseRequestData(), actual.GetCloseRequestData())
}
@@ -41,21 +41,21 @@ func TestMakeSessionCloseInfo(t *testing.T) {
{ConnectionId: "bar", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED},
},
}
actual, err := new(Worker).makeSessionCloseInfo(closeInfo, response)
actual, err := makeSessionCloseInfo(closeInfo, response)
require.NoError(err)
require.Equal(expected, actual)
}
func TestMakeSessionCloseInfoErrorIfCloseInfoNil(t *testing.T) {
require := require.New(t)
actual, err := new(Worker).makeSessionCloseInfo(nil, nil)
actual, err := makeSessionCloseInfo(nil, nil)
require.Nil(actual)
require.ErrorIs(err, errMakeSessionCloseInfoNilCloseInfo)
}
func TestMakeSessionCloseInfoEmpty(t *testing.T) {
require := require.New(t)
actual, err := new(Worker).makeSessionCloseInfo(make(map[string]string), nil)
actual, err := makeSessionCloseInfo(make(map[string]string), nil)
require.NoError(err)
require.Equal(
make(map[string][]*pbs.CloseConnectionResponseData),
@@ -74,21 +74,21 @@ func TestMakeFakeSessionCloseInfo(t *testing.T) {
{ConnectionId: "bar", Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED},
},
}
actual, err := new(Worker).makeFakeSessionCloseInfo(closeInfo)
actual, err := makeFakeSessionCloseInfo(closeInfo)
require.NoError(err)
require.Equal(expected, actual)
}
func TestMakeFakeSessionCloseInfoErrorIfCloseInfoNil(t *testing.T) {
require := require.New(t)
actual, err := new(Worker).makeFakeSessionCloseInfo(nil)
actual, err := makeFakeSessionCloseInfo(nil)
require.Nil(actual)
require.ErrorIs(err, errMakeSessionCloseInfoNilCloseInfo)
}
func TestMakeFakeSessionCloseInfoEmpty(t *testing.T) {
require := require.New(t)
actual, err := new(Worker).makeFakeSessionCloseInfo(make(map[string]string))
actual, err := makeFakeSessionCloseInfo(make(map[string]string))
require.NoError(err)
require.Equal(
make(map[string][]*pbs.CloseConnectionResponseData),
@@ -117,22 +117,22 @@ func TestWorkerSetCloseTimeForResponse(t *testing.T) {
},
sessionInfoMap: func() *sync.Map {
m := new(sync.Map)
m.Store("one", &sessionInfo{
id: "one",
connInfoMap: map[string]*connInfo{
"foo": {id: "foo"},
m.Store("one", &Info{
Id: "one",
ConnInfoMap: map[string]*ConnInfo{
"foo": {Id: "foo"},
},
})
m.Store("two", &sessionInfo{
id: "two",
connInfoMap: map[string]*connInfo{
"bar": {id: "bar"},
m.Store("two", &Info{
Id: "two",
ConnInfoMap: map[string]*ConnInfo{
"bar": {Id: "bar"},
},
})
m.Store("three", &sessionInfo{
id: "three",
connInfoMap: map[string]*connInfo{
"baz": {id: "baz"},
m.Store("three", &Info{
Id: "three",
ConnInfoMap: map[string]*ConnInfo{
"baz": {Id: "baz"},
},
})
@@ -156,16 +156,16 @@ func TestWorkerSetCloseTimeForResponse(t *testing.T) {
},
sessionInfoMap: func() *sync.Map {
m := new(sync.Map)
m.Store("one", &sessionInfo{
id: "one",
connInfoMap: map[string]*connInfo{
"foo": {id: "foo"},
m.Store("one", &Info{
Id: "one",
ConnInfoMap: map[string]*ConnInfo{
"foo": {Id: "foo"},
},
})
m.Store("two", &sessionInfo{
id: "two",
connInfoMap: map[string]*connInfo{
"bar": {id: "bar"},
m.Store("two", &Info{
Id: "two",
ConnInfoMap: map[string]*ConnInfo{
"bar": {Id: "bar"},
},
})
@@ -188,10 +188,10 @@ func TestWorkerSetCloseTimeForResponse(t *testing.T) {
},
sessionInfoMap: func() *sync.Map {
m := new(sync.Map)
m.Store("one", &sessionInfo{
id: "one",
connInfoMap: map[string]*connInfo{
"foo": {id: "foo"},
m.Store("one", &Info{
Id: "one",
ConnInfoMap: map[string]*ConnInfo{
"foo": {Id: "foo"},
},
})
@@ -217,13 +217,13 @@ func TestWorkerSetCloseTimeForResponse(t *testing.T) {
},
sessionInfoMap: func() *sync.Map {
m := new(sync.Map)
m.Store("one", &sessionInfo{
id: "one",
connInfoMap: map[string]*connInfo{
"foo": {id: "foo"},
m.Store("one", &Info{
Id: "one",
ConnInfoMap: map[string]*ConnInfo{
"foo": {Id: "foo"},
},
})
m.Store("two", &sessionInfo{id: "two"})
m.Store("two", &Info{Id: "two"})
return m
},
@@ -240,10 +240,10 @@ func TestWorkerSetCloseTimeForResponse(t *testing.T) {
sessionCloseInfo: make(map[string][]*pbs.CloseConnectionResponseData),
sessionInfoMap: func() *sync.Map {
m := new(sync.Map)
m.Store("one", &sessionInfo{
id: "one",
connInfoMap: map[string]*connInfo{
"foo": {id: "foo"},
m.Store("one", &Info{
Id: "one",
ConnInfoMap: map[string]*ConnInfo{
"foo": {Id: "foo"},
},
})
@@ -258,19 +258,17 @@ func TestWorkerSetCloseTimeForResponse(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
w := &Worker{
sessionInfoMap: tc.sessionInfoMap(),
}
actual, actualErr := w.setCloseTimeForResponse(tc.sessionCloseInfo)
sessionInfoMap := tc.sessionInfoMap()
actual, actualErr := setCloseTimeForResponse(sessionInfoMap, tc.sessionCloseInfo)
// Assert all close times were set
w.sessionInfoMap.Range(func(key, value interface{}) bool {
sessionInfoMap.Range(func(key, value interface{}) bool {
t.Helper()
for _, ci := range value.(*sessionInfo).connInfoMap {
if _, ok := tc.expectedClosed[ci.id]; ok {
require.NotEqual(time.Time{}, ci.closeTime)
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)
require.Equal(time.Time{}, ci.CloseTime)
}
}
+38 -37
View File
@@ -9,16 +9,12 @@ import (
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/boundary/internal/servers"
"github.com/hashicorp/boundary/internal/servers/worker/common"
"github.com/hashicorp/boundary/internal/servers/worker/session"
"github.com/hashicorp/boundary/internal/types/resource"
"google.golang.org/grpc/resolver"
)
// In the future we could make this configurable
const (
statusInterval = 2 * time.Second
statusTimeout = 5 * time.Second
)
type LastStatusInformation struct {
*pbs.StatusResponse
StatusTime time.Time
@@ -28,7 +24,7 @@ func (w *Worker) startStatusTicking(cancelCtx context.Context) {
const op = "worker.(Worker).startStatusTicking"
r := rand.New(rand.NewSource(time.Now().UnixNano()))
// This function exists to desynchronize calls to controllers from
// workers so we aren't always getting status updates at the exact same
// workers, so we aren't always getting status updates at the exact same
// intervals, to ease the load on the DB.
getRandomInterval := func() time.Duration {
// 0 to 0.5 adjustment to the base
@@ -37,14 +33,14 @@ func (w *Worker) startStatusTicking(cancelCtx context.Context) {
if r.Float32() > 0.5 {
f = -1 * f
}
return statusInterval + time.Duration(f*float64(time.Second))
return common.StatusInterval + time.Duration(f*float64(time.Second))
}
timer := time.NewTimer(0)
for {
select {
case <-cancelCtx.Done():
event.WriteSysEvent(context.TODO(), op, "status ticking shutting down")
event.WriteSysEvent(w.baseContext, op, "status ticking shutting down")
return
case <-timer.C:
@@ -102,14 +98,14 @@ func (w *Worker) sendWorkerStatus(cancelCtx context.Context) {
w.sessionInfoMap.Range(func(key, value interface{}) bool {
var jobInfo pbs.SessionJobInfo
sessionId := key.(string)
si := value.(*sessionInfo)
si := value.(*session.Info)
si.RLock()
status := si.status
connections := make([]*pbs.Connection, 0, len(si.connInfoMap))
for k, v := range si.connInfoMap {
status := si.Status
connections := make([]*pbs.Connection, 0, len(si.ConnInfoMap))
for k, v := range si.ConnInfoMap {
connections = append(connections, &pbs.Connection{
ConnectionId: k,
Status: v.status,
Status: v.Status,
})
}
si.RUnlock()
@@ -137,7 +133,7 @@ func (w *Worker) sendWorkerStatus(cancelCtx context.Context) {
if w.updateTags.Load() {
tags = w.tags.Load().(map[string]*servers.TagValues)
}
statusCtx, statusCancel := context.WithTimeout(cancelCtx, statusTimeout)
statusCtx, statusCancel := context.WithTimeout(cancelCtx, common.StatusTimeout)
defer statusCancel()
result, err := client.Status(statusCtx, &pbs.StatusRequest{
Jobs: activeJobs,
@@ -199,20 +195,20 @@ func (w *Worker) sendWorkerStatus(cancelCtx context.Context) {
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.(*sessionInfo)
si := siRaw.(*session.Info)
si.Lock()
si.status = sessInfo.GetStatus()
si.Status = 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]
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
}
connInfo.status = conn.GetStatus()
connInfo.Status = conn.GetStatus()
}
si.Unlock()
@@ -238,38 +234,38 @@ func (w *Worker) cleanupConnections(cancelCtx context.Context, ignoreSessionStat
closeInfo := make(map[string]string)
cleanSessionIds := make([]string, 0)
w.sessionInfoMap.Range(func(key, value interface{}) bool {
si := value.(*sessionInfo)
si := value.(*session.Info)
si.Lock()
defer si.Unlock()
switch {
case ignoreSessionState,
si.status == pbs.SESSIONSTATUS_SESSIONSTATUS_CANCELING,
si.status == pbs.SESSIONSTATUS_SESSIONSTATUS_TERMINATED,
time.Until(si.lookupSessionResponse.Expiration.AsTime()) < 0:
si.Status == pbs.SESSIONSTATUS_SESSIONSTATUS_CANCELING,
si.Status == pbs.SESSIONSTATUS_SESSIONSTATUS_TERMINATED,
time.Until(si.LookupSessionResponse.Expiration.AsTime()) < 0:
// Cancel connections without regard to individual connection
// state.
closedIds := w.cancelConnections(si.connInfoMap, true)
closedIds := w.cancelConnections(si.ConnInfoMap, true)
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] = si.Id
event.WriteSysEvent(cancelCtx, op, "terminated connection due to cancellation or expiration", "session_id", si.Id, "connection_id", connId)
}
// closeTime is marked by closeConnections iff the
// 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
// are marked closed, clean up the session.
if len(closedIds) == 0 {
cleanSessionIds = append(cleanSessionIds, si.id)
cleanSessionIds = append(cleanSessionIds, si.Id)
}
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)
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)
closeInfo[connId] = si.Id
event.WriteSysEvent(cancelCtx, op, "terminated connection due to cancellation or expiration", "session_id", si.Id, "connection_id", connId)
}
}
@@ -282,7 +278,12 @@ 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.
w.closeConnections(cancelCtx, closeInfo)
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)
}
}
// Forget sessions where the session is expired/canceled and all
@@ -293,21 +294,21 @@ func (w *Worker) cleanupConnections(cancelCtx context.Context, ignoreSessionStat
}
// cancelConnections is run by cleanupConnections to iterate over a
// session's connInfoMap and close connections based on the
// 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]*connInfo, ignoreConnectionState bool) []string {
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 {
if v.CloseTime.IsZero() {
if !ignoreConnectionState && v.Status != pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED {
continue
}
v.connCancel()
v.ConnCancel()
closedIds = append(closedIds, k)
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ import (
)
func TestWorkerWaitForNextSuccessfulStatusUpdate(t *testing.T) {
// do not runn using t.Parallel() since it relies on the sys eventer
// do not run using t.Parallel() since it relies on the sys eventer
event.TestEnableEventing(t, true)
testConfig := event.DefaultEventerConfig()
testLock := &sync.Mutex{}
-81
View File
@@ -1,81 +0,0 @@
package worker
import (
"context"
"io"
"net"
"net/url"
"sync"
"nhooyr.io/websocket"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/internal/observability/event"
)
func (w *Worker) handleTcpProxyV1(connCtx context.Context, clientAddr *net.TCPAddr, conn *websocket.Conn, si *sessionInfo, connectionId, endpoint string) {
const op = "worker.(Worker).handleTcpProxyV1"
ctx := context.TODO()
si.RLock()
sessionId := si.lookupSessionResponse.GetAuthorization().GetSessionId()
si.RUnlock()
sessionUrl, err := url.Parse(endpoint)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error parsing endpoint information", "session_id", sessionId, "endpoint", endpoint))
conn.Close(websocket.StatusInternalError, "cannot parse endpoint url")
return
}
if sessionUrl.Scheme != "tcp" {
event.WriteError(ctx, op, err, event.WithInfo("session_id", sessionId, "endpoint", endpoint))
conn.Close(websocket.StatusInternalError, "invalid scheme for type")
return
}
remoteConn, err := net.Dial("tcp", sessionUrl.Host)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error dialing endpoint", "endpoint", endpoint))
conn.Close(websocket.StatusInternalError, "endpoint dialing failed")
return
}
// Assert this for better Go 1.11 splice support
tcpRemoteConn := remoteConn.(*net.TCPConn)
endpointAddr := tcpRemoteConn.RemoteAddr().(*net.TCPAddr)
connectionInfo := &pbs.ConnectConnectionRequest{
ConnectionId: connectionId,
ClientTcpAddress: clientAddr.IP.String(),
ClientTcpPort: uint32(clientAddr.Port),
EndpointTcpAddress: endpointAddr.IP.String(),
EndpointTcpPort: uint32(endpointAddr.Port),
Type: "tcp",
}
connStatus, err := w.connectConnection(connCtx, connectionInfo)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error marking connection as connected"))
conn.Close(websocket.StatusInternalError, "failed to mark connection as connected")
return
}
si.Lock()
si.connInfoMap[connectionId].status = connStatus
si.Unlock()
// Get a wrapped net.Conn so we can use io.Copy
netConn := websocket.NetConn(connCtx, conn, websocket.MessageBinary)
connWg := new(sync.WaitGroup)
connWg.Add(2)
go func() {
defer connWg.Done()
_, _ = io.Copy(netConn, tcpRemoteConn)
netConn.Close()
tcpRemoteConn.Close()
}()
go func() {
defer connWg.Done()
_, _ = io.Copy(tcpRemoteConn, netConn)
tcpRemoteConn.Close()
netConn.Close()
}()
connWg.Wait()
}
+10 -9
View File
@@ -11,6 +11,7 @@ import (
"github.com/hashicorp/boundary/internal/cmd/config"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/boundary/internal/servers/worker/session"
"github.com/hashicorp/go-hclog"
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/hashicorp/go-secure-stdlib/base62"
@@ -83,7 +84,7 @@ func (tw *TestWorker) ProxyAddrs() []string {
// the worker's local session state. This detail is a point-in-time
// snapshot of what's in sessionInfoMap for a particular session, and
// may not contain all of the information that is contained within
// it, or the underlying connInfoMap. Only details that are really
// it, or the underlying ConnInfoMap. Only details that are really
// important to testing are passed along.
type TestSessionInfo struct {
Id string
@@ -117,21 +118,21 @@ func (tw *TestWorker) LookupSession(id string) (TestSessionInfo, bool) {
return result, false
}
sess := raw.(*sessionInfo)
sess := raw.(*session.Info)
sess.RLock()
defer sess.RUnlock()
conns := make(map[string]TestConnectionInfo)
for _, conn := range sess.connInfoMap {
conns[conn.id] = TestConnectionInfo{
Id: conn.id,
Status: conn.status,
CloseTime: conn.closeTime,
for _, conn := range sess.ConnInfoMap {
conns[conn.Id] = TestConnectionInfo{
Id: conn.Id,
Status: conn.Status,
CloseTime: conn.CloseTime,
}
}
result.Id = sess.id
result.Status = sess.status
result.Id = sess.Id
result.Status = sess.Status
result.Connections = conns
return result, true
+8 -7
View File
@@ -6,6 +6,7 @@ import (
"time"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/internal/servers/worker/session"
"github.com/stretchr/testify/require"
)
@@ -20,14 +21,14 @@ func TestTestWorkerLookupSession(t *testing.T) {
tw.w = &Worker{
sessionInfoMap: new(sync.Map),
}
tw.w.sessionInfoMap.Store("foo", &sessionInfo{
id: "foo",
status: pbs.SESSIONSTATUS_SESSIONSTATUS_ACTIVE,
connInfoMap: map[string]*connInfo{
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,
Id: "one",
Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED,
CloseTime: refTime,
},
},
})
+107 -4
View File
@@ -2,16 +2,23 @@ package worker
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/hashicorp/boundary/internal/cmd/config"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/boundary/internal/servers"
"github.com/hashicorp/boundary/internal/servers/worker/session"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-secure-stdlib/mlock"
ua "go.uber.org/atomic"
@@ -100,7 +107,7 @@ func New(conf *Config) (*Worker, error) {
func (w *Worker) Start() error {
const op = "worker.(Worker).Start"
if w.started.Load() {
event.WriteSysEvent(context.TODO(), op, "already started, skipping")
event.WriteSysEvent(w.baseContext, op, "already started, skipping")
return nil
}
@@ -135,9 +142,8 @@ func (w *Worker) Start() error {
// doable, but work for later.
func (w *Worker) Shutdown(skipListeners bool) error {
const op = "worker.(Worker).Shutdown"
ctx := context.TODO()
if !w.started.Load() {
event.WriteSysEvent(context.TODO(), op, "already shut down, skipping")
event.WriteSysEvent(w.baseContext, op, "already shut down, skipping")
return nil
}
@@ -163,7 +169,7 @@ func (w *Worker) Shutdown(skipListeners bool) error {
defer nextStatusCancel()
for {
if err := nextStatusCtx.Err(); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error waiting for next status report to controller"))
event.WriteError(w.baseContext, op, err, event.WithInfoMsg("error waiting for next status report to controller"))
break
}
@@ -211,3 +217,100 @@ func (w *Worker) ParseAndStoreTags(incoming map[string][]string) {
w.tags.Store(tags)
w.updateTags.Store(true)
}
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
}
func (w *Worker) getSessionTls(hello *tls.ClientHelloInfo) (*tls.Config, error) {
const op = "worker.(Worker).getSessionTls"
ctx := w.baseContext
var sessionId string
switch {
case strings.HasPrefix(hello.ServerName, "s_"):
sessionId = hello.ServerName
default:
event.WriteSysEvent(ctx, op, "invalid session in SNI", "session_id", hello.ServerName)
return nil, fmt.Errorf("could not find session ID in SNI")
}
conn, err := w.ControllerSessionConn()
if err != nil {
event.WriteError(ctx, op, err, event.WithInfo("failed to create controller session client"))
}
timeoutContext, cancel := context.WithTimeout(w.baseContext, session.ValidateSessionTimeout)
defer cancel()
resp, err := conn.LookupSession(timeoutContext, &pbs.LookupSessionRequest{
ServerId: w.conf.RawConfig.Worker.Name,
SessionId: sessionId,
})
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)
}
if len(parsedCert.DNSNames) != 1 {
return nil, fmt.Errorf("invalid length of DNS names (%d) in parsed certificate", len(parsedCert.DNSNames))
}
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,
},
},
ServerName: parsedCert.DNSNames[0],
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: certPool,
MinVersion: tls.VersionTLS13,
}
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()
}
return tlsConf, nil
}