2020-09-03 13:24:34 -04:00
package proxy
import (
2020-09-19 13:51:50 -04:00
"context"
2020-09-03 13:24:34 -04:00
"crypto/ed25519"
"crypto/tls"
"crypto/x509"
2020-09-19 00:18:17 -04:00
"encoding/json"
2020-09-20 12:51:08 -04:00
"errors"
2020-09-03 13:24:34 -04:00
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"os"
2020-09-24 17:29:12 -04:00
"os/exec"
"strconv"
2020-09-03 13:24:34 -04:00
"strings"
"sync"
2020-09-24 17:29:12 -04:00
"syscall"
2020-09-20 16:28:00 -04:00
"time"
2020-09-03 13:24:34 -04:00
2020-09-24 14:34:58 -07:00
"github.com/hashicorp/boundary/api"
2020-09-19 00:18:17 -04:00
"github.com/hashicorp/boundary/api/targets"
2020-09-03 13:24:34 -04:00
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/cmd/base"
2020-09-19 00:18:17 -04:00
targetspb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/targets"
2020-09-10 23:25:40 -04:00
"github.com/hashicorp/boundary/internal/proxy"
2020-09-03 13:24:34 -04:00
"github.com/hashicorp/go-cleanhttp"
2020-09-10 23:25:40 -04:00
"github.com/hashicorp/vault/sdk/helper/base62"
2020-09-03 13:24:34 -04:00
"github.com/mitchellh/cli"
2020-09-26 14:01:49 -04:00
"github.com/mr-tron/base58"
2020-09-03 13:24:34 -04:00
"github.com/posener/complete"
2020-09-20 16:28:00 -04:00
"go.uber.org/atomic"
2020-09-20 12:51:08 -04:00
"google.golang.org/protobuf/proto"
2020-09-03 13:24:34 -04:00
"nhooyr.io/websocket"
2020-09-10 23:25:40 -04:00
"nhooyr.io/websocket/wspb"
2020-09-03 13:24:34 -04:00
)
2020-09-20 16:28:00 -04:00
type SessionInfo struct {
2020-09-20 21:18:07 -04:00
Address string `json:"address"`
Port int `json:"port"`
Protocol string `json:"protocol"`
Expiration time . Time `json:"expiration"`
ConnectionLimit int32 `json:"connection_limit"`
2020-09-21 02:22:59 -04:00
SessionId string `json:"session_id"`
2020-09-19 00:18:17 -04:00
}
2020-09-20 16:28:00 -04:00
type ConnectionInfo struct {
2020-09-20 21:18:07 -04:00
ConnectionsLeft int32 `json:"connections_left"`
}
type TerminationInfo struct {
Reason string `json:"termination_reason"`
2020-09-20 16:28:00 -04:00
}
2020-09-03 13:24:34 -04:00
var _ cli . Command = ( * Command )( nil )
var _ cli . CommandAutocomplete = ( * Command )( nil )
type Command struct {
* base . Command
2020-09-19 00:18:17 -04:00
flagAuthz string
2020-09-03 13:24:34 -04:00
flagListenAddr string
flagListenPort int
2020-09-19 00:18:17 -04:00
flagTargetId string
flagHostId string
2020-09-24 17:29:12 -04:00
flagExec string
2020-09-25 12:26:16 -04:00
flagUsername string
2020-09-24 17:29:12 -04:00
2020-10-01 00:27:13 -04:00
// HTTP
flagHttpStyle string
flagHttpHost string
2020-10-01 12:16:18 -04:00
flagHttpPath string
2020-10-01 00:27:13 -04:00
flagHttpMethod string
flagHttpScheme string
2020-09-24 17:29:12 -04:00
// SSH
flagSshStyle string
2020-09-19 00:18:17 -04:00
2020-09-24 21:16:12 -04:00
// Postgres
2020-09-25 12:26:16 -04:00
flagPostgresStyle string
2020-09-24 21:16:12 -04:00
// RDP
flagRdpStyle string
2020-09-19 00:18:17 -04:00
Func string
2020-09-20 12:51:08 -04:00
2020-10-01 17:07:34 -04:00
sessionAuthz * targets . SessionAuthorization
2020-09-24 17:29:12 -04:00
connWg * sync . WaitGroup
listenerCloseOnce sync . Once
listener * net . TCPListener
listenerAddr * net . TCPAddr
connsLeftCh chan int32
connectionsLeft atomic . Int32
expiration time . Time
execCmdReturnValue * atomic . Int32
proxyCtx context . Context
proxyCancel context . CancelFunc
2020-09-03 13:24:34 -04:00
}
func ( c * Command ) Synopsis () string {
2020-09-19 00:18:17 -04:00
switch c . Func {
case "proxy" :
return "Launch the Boundary CLI in proxy mode"
case "connect" :
return "Authorize a session against a target and launch a proxied connection"
2020-10-01 00:27:13 -04:00
case "http" :
return "Authorize a session against a target and invoke an HTTP client to connect"
2020-09-24 17:29:12 -04:00
case "ssh" :
2020-09-24 21:16:12 -04:00
return "Authorize a session against a target and invoke an SSH client to connect"
case "postgres" :
2020-10-01 00:27:13 -04:00
return "Authorize a session against a target and invoke a Postgres client to connect"
2020-09-24 21:16:12 -04:00
case "rdp" :
return "Authorize a session against a target and invoke an RDP client to connect"
2020-09-19 00:18:17 -04:00
}
return ""
2020-09-03 13:24:34 -04:00
}
func ( c * Command ) Help () string {
2020-09-19 00:18:17 -04:00
switch c . Func {
case "proxy" :
return base . WrapForHelpText ([] string {
"Usage: boundary proxy [options] [args]" ,
"" ,
" This command allows launching the Boundary CLI in proxy mode. In this mode, the CLI expects to take in an authorization string returned from a Boundary controller. The CLI will then create a connection to a Boundary worker and ready a listening port for a local connection." ,
"" ,
" Example:" ,
"" ,
` $ boundary proxy -auth "UgxzX29mVEpwNUt6QlGiAQ..."` ,
}) + c . Flags (). Help ()
case "connect" :
return base . WrapForHelpText ([] string {
"Usage: boundary connect [options] [args]" ,
"" ,
` This command performs a target authorization and proxy launch in one command; it is equivalent to sending the output of "boundary targets authorize" into "boundary proxy". See the help output for those commands for more information.` ,
"" ,
" Example:" ,
"" ,
` $ boundary connect -target-id ttcp_1234567890"` ,
}) + c . Flags (). Help ()
}
return ""
2020-09-03 13:24:34 -04:00
}
func ( c * Command ) Flags () * base . FlagSets {
2020-09-19 00:18:17 -04:00
bits := base . FlagSetOutputFormat
if c . Func == "connect" {
bits = base . FlagSetHTTP | base . FlagSetClient | bits
}
set := c . FlagSet ( bits )
2020-09-03 13:24:34 -04:00
2020-09-19 00:18:17 -04:00
switch c . Func {
case "proxy" :
2020-09-24 17:29:12 -04:00
f := set . NewFlagSet ( "Proxy Options" )
2020-09-19 00:18:17 -04:00
f . StringVar ( & base . StringVar {
Name : "authz" ,
Target : & c . flagAuthz ,
EnvVar : "BOUNDARY_PROXY_AUTHZ" ,
Completion : complete . PredictAnything ,
Usage : `The authorization string returned from the Boundary controller. If set to "-", the command will attempt to read in the authorization string from standard input.` ,
})
2020-09-24 17:29:12 -04:00
f . StringVar ( & base . StringVar {
Name : "listen-addr" ,
Target : & c . flagListenAddr ,
EnvVar : "BOUNDARY_PROXY_LISTEN_ADDR" ,
Completion : complete . PredictAnything ,
Usage : `If set, the CLI will attempt to bind its listening address to the given value, which must be an IP address. If it cannot, the command will error. If not set, defaults to the IPv4 loopback address (127.0.0.1).` ,
})
f . IntVar ( & base . IntVar {
Name : "listen-port" ,
Target : & c . flagListenPort ,
EnvVar : "BOUNDARY_PROXY_LISTEN_PORT" ,
Completion : complete . PredictAnything ,
Usage : `If set, the CLI will attempt to bind its listening port to the given value. If it cannot, the command will error.` ,
})
2020-10-01 00:27:13 -04:00
case "connect" , "http" , "ssh" , "rdp" , "postgres" :
2020-09-24 17:29:12 -04:00
f := set . NewFlagSet ( "Connect Options" )
2020-09-19 00:18:17 -04:00
f . StringVar ( & base . StringVar {
Name : "target-id" ,
Target : & c . flagTargetId ,
Usage : "The ID of the target to authorize against." ,
})
f . StringVar ( & base . StringVar {
Name : "host-id" ,
Target : & c . flagHostId ,
Usage : "The ID of a specific host to connect to out of the hosts from the target's host sets. If not specified, one is chosen at random." ,
})
2020-09-24 17:29:12 -04:00
f . StringVar ( & base . StringVar {
Name : "listen-addr" ,
Target : & c . flagListenAddr ,
EnvVar : "BOUNDARY_CONNECT_LISTEN_ADDR" ,
Completion : complete . PredictAnything ,
Usage : `If set, the CLI will attempt to bind its listening address to the given value, which must be an IP address. If it cannot, the command will error. If not set, defaults to the most common IPv4 loopback address (127.0.0.1).` ,
})
f . IntVar ( & base . IntVar {
Name : "listen-port" ,
Target : & c . flagListenPort ,
EnvVar : "BOUNDARY_CONNECT_LISTEN_PORT" ,
Completion : complete . PredictAnything ,
Usage : `If set, the CLI will attempt to bind its listening port to the given value. If it cannot, the command will error.` ,
})
f . StringVar ( & base . StringVar {
Name : "exec" ,
Target : & c . flagExec ,
EnvVar : "BOUNDARY_CONNECT_EXEC" ,
Completion : complete . PredictAnything ,
Usage : `If set, after connecting to the worker, the given binary will be executed. This should be a binary on your path, or an absolute path. If all command flags are followed by " -- " (space, two hyphens, space), then any arguments after that will be sent directly to the binary.` ,
})
2020-09-19 00:18:17 -04:00
}
2020-09-03 13:24:34 -04:00
2020-09-24 21:16:12 -04:00
switch c . Func {
2020-10-01 00:27:13 -04:00
case "http" :
f := set . NewFlagSet ( "HTTP Options" )
f . StringVar ( & base . StringVar {
Name : "style" ,
Target : & c . flagHttpStyle ,
EnvVar : "BOUNDARY_CONNECT_HTTP_STYLE" ,
Completion : complete . PredictSet ( "curl" ),
Default : "curl" ,
Usage : `Specifies how the CLI will attempt to invoke an HTTP client. This will also set a suitable default for -exec if a value was not specified. Currently-understood values are "curl".` ,
})
f . StringVar ( & base . StringVar {
Name : "host" ,
Target : & c . flagHttpHost ,
EnvVar : "BOUNDARY_CONNECT_HTTP_HOST" ,
Completion : complete . PredictNothing ,
Usage : `Specifies the host value to use. The specified hostname will be passed through to the client (if supported) for use in the Host header and TLS SNI value.` ,
})
2020-10-01 12:16:18 -04:00
f . StringVar ( & base . StringVar {
Name : "path" ,
Target : & c . flagHttpPath ,
EnvVar : "BOUNDARY_CONNECT_HTTP_PATH" ,
Completion : complete . PredictNothing ,
Usage : `Specifies a path that will be appended to the generated URL.` ,
})
2020-10-01 00:27:13 -04:00
f . StringVar ( & base . StringVar {
Name : "method" ,
Target : & c . flagHttpMethod ,
EnvVar : "BOUNDARY_CONNECT_HTTP_METHOD" ,
Completion : complete . PredictNothing ,
Usage : `Specifies the method to use. If not set, will use the client's default.` ,
})
f . StringVar ( & base . StringVar {
Name : "scheme" ,
Target : & c . flagHttpScheme ,
Default : "https" ,
EnvVar : "BOUNDARY_CONNECT_HTTP_SCHEME" ,
Completion : complete . PredictNothing ,
Usage : `Specifies the scheme to use.` ,
})
2020-09-24 21:16:12 -04:00
case "ssh" :
2020-09-24 17:29:12 -04:00
f := set . NewFlagSet ( "SSH Options" )
2020-09-03 13:24:34 -04:00
2020-09-24 17:29:12 -04:00
f . StringVar ( & base . StringVar {
Name : "style" ,
Target : & c . flagSshStyle ,
EnvVar : "BOUNDARY_CONNECT_SSH_STYLE" ,
2020-09-24 21:16:12 -04:00
Completion : complete . PredictSet ( "ssh" , "putty" ),
2020-09-24 17:29:12 -04:00
Default : "ssh" ,
2020-09-24 21:16:12 -04:00
Usage : `Specifies how the CLI will attempt to invoke an SSH client. This will also set a suitable default for -exec if a value was not specified. Currently-understood values are "ssh" and "putty".` ,
})
2020-09-25 12:26:16 -04:00
f . StringVar ( & base . StringVar {
Name : "username" ,
Target : & c . flagUsername ,
EnvVar : "BOUNDARY_CONNECT_USERNAME" ,
Completion : complete . PredictNothing ,
Usage : `Specifies the username to pass through to the client` ,
})
2020-09-24 21:16:12 -04:00
case "postgres" :
f := set . NewFlagSet ( "Postgres Options" )
f . StringVar ( & base . StringVar {
Name : "style" ,
Target : & c . flagPostgresStyle ,
EnvVar : "BOUNDARY_CONNECT_POSTGRES_STYLE" ,
Completion : complete . PredictSet ( "psql" ),
Default : "psql" ,
Usage : `Specifies how the CLI will attempt to invoke a Postgres client. This will also set a suitable default for -exec if a value was not specified. Currently-understood values are "psql".` ,
})
f . StringVar ( & base . StringVar {
Name : "username" ,
2020-09-25 12:26:16 -04:00
Target : & c . flagUsername ,
EnvVar : "BOUNDARY_CONNECT_USERNAME" ,
2020-09-24 21:16:12 -04:00
Completion : complete . PredictNothing ,
2020-09-25 12:26:16 -04:00
Usage : `Specifies the username to pass through to the client` ,
2020-09-24 21:16:12 -04:00
})
case "rdp" :
f := set . NewFlagSet ( "RDP Options" )
f . StringVar ( & base . StringVar {
Name : "style" ,
Target : & c . flagRdpStyle ,
EnvVar : "BOUNDARY_CONNECT_RDP_STYLE" ,
Completion : complete . PredictSet ( "mstsc" ),
Default : "mstsc" ,
Usage : `Specifies how the CLI will attempt to invoke an RDP client. This will also set a suitable default for -exec if a value was not specified. Currently-understood values are "mstsc".` ,
2020-09-24 17:29:12 -04:00
})
}
2020-09-03 13:24:34 -04:00
2020-09-24 17:29:12 -04:00
/*
f.BoolVar(&base.BoolVar{
Name: "verbose",
Target: &c.flagVerbose,
Completion: complete.PredictAnything,
Usage: "Turns on some extra verbosity in the command output.",
})
*/
2020-09-03 13:24:34 -04:00
return set
}
func ( c * Command ) AutocompleteArgs () complete . Predictor {
return complete . PredictAnything
}
func ( c * Command ) AutocompleteFlags () complete . Flags {
return c . Flags (). Completions ()
}
2020-09-20 12:51:08 -04:00
func ( c * Command ) Run ( args [] string ) ( retCode int ) {
2020-09-24 17:29:12 -04:00
var passthroughArgs [] string
for i , v := range args {
if v == "--" {
passthroughArgs = args [ i + 1 :]
args = args [: i ]
}
}
2020-09-03 13:24:34 -04:00
f := c . Flags ()
if err := f . Parse ( args ); err != nil {
c . UI . Error ( err . Error ())
return 1
}
2020-09-24 17:29:12 -04:00
switch c . Func {
2020-10-01 00:27:13 -04:00
case "http" :
if c . flagExec == "" {
c . flagExec = strings . ToLower ( c . flagHttpStyle )
}
2020-09-24 17:29:12 -04:00
case "ssh" :
if c . flagExec == "" {
c . flagExec = strings . ToLower ( c . flagSshStyle )
}
2020-09-24 21:16:12 -04:00
case "postgres" :
if c . flagExec == "" {
c . flagExec = strings . ToLower ( c . flagPostgresStyle )
}
case "rdp" :
if c . flagExec == "" {
c . flagExec = strings . ToLower ( c . flagRdpStyle )
}
2020-09-24 17:29:12 -04:00
}
2020-09-20 12:51:08 -04:00
tofuToken , err := base62 . Random ( 20 )
if err != nil {
2020-09-10 23:25:40 -04:00
c . UI . Error ( fmt . Errorf ( "Could not derive random bytes for tofu token: %w" , err ). Error ())
return 1
}
2020-09-20 21:18:07 -04:00
c . connsLeftCh = make ( chan int32 )
2020-09-03 13:24:34 -04:00
if c . flagListenAddr == "" {
c . flagListenAddr = "127.0.0.1"
}
listenAddr := net . ParseIP ( c . flagListenAddr )
if listenAddr == nil {
c . UI . Error ( fmt . Sprintf ( "Could not successfully parse listen address of %s" , c . flagListenAddr ))
return 1
}
2020-09-19 00:18:17 -04:00
authzString := c . flagAuthz
switch c . Func {
case "proxy" :
if authzString == "-" {
authBytes , err := ioutil . ReadAll ( os . Stdin )
if err != nil {
c . UI . Error ( fmt . Errorf ( "No authorization string was provided and encountered the following error attempting to read it from stdin: %w" , err ). Error ())
return 1
}
if len ( authBytes ) == 0 {
c . UI . Error ( "No authorization data read from stdin" )
return 1
}
authzString = string ( authBytes )
}
if authzString == "" {
c . UI . Error ( "Authorization data was empty" )
2020-09-03 13:24:34 -04:00
return 1
}
2020-09-19 00:18:17 -04:00
if authzString [ 0 ] == '{' {
// Attempt to decode the JSON output of an authorize call and pull the
// token out of there
2020-10-01 17:07:34 -04:00
c . sessionAuthz = new ( targets . SessionAuthorization )
if err := json . Unmarshal ([] byte ( authzString ), c . sessionAuthz ); err == nil {
authzString = c . sessionAuthz . AuthorizationToken
2020-09-19 00:18:17 -04:00
}
}
2020-10-01 00:27:13 -04:00
case "connect" , "http" , "ssh" , "postgres" , "rdp" :
2020-09-19 00:18:17 -04:00
if c . flagTargetId == "" {
c . UI . Error ( "Target ID must be provided" )
2020-09-03 13:24:34 -04:00
return 1
}
2020-09-19 00:18:17 -04:00
client , err := c . Client ()
if err != nil {
c . UI . Error ( fmt . Sprintf ( "Error creating API client: %s" , err . Error ()))
return 2
}
targetClient := targets . NewClient ( client )
var opts [] targets . Option
if len ( c . flagHostId ) != 0 {
opts = append ( opts , targets . WithHostId ( c . flagHostId ))
}
2020-09-24 14:34:58 -07:00
sar , err := targetClient . Authorize ( c . Context , c . flagTargetId , opts ... )
2020-09-19 00:18:17 -04:00
if err != nil {
2020-09-24 14:34:58 -07:00
if api . AsServerError ( err ) != nil {
c . UI . Error ( fmt . Sprintf ( "Error from controller when performing authorize on a session against target: %s" , err . Error ()))
return 1
}
2020-09-19 00:18:17 -04:00
c . UI . Error ( fmt . Sprintf ( "Error trying to authorize a session against target: %s" , err . Error ()))
return 2
}
2020-10-01 17:07:34 -04:00
c . sessionAuthz = sar . GetItem ().( * targets . SessionAuthorization )
authzString = c . sessionAuthz . AuthorizationToken
2020-09-03 13:24:34 -04:00
}
2020-09-26 14:01:49 -04:00
marshaled , err := base58 . FastBase58Decoding ( authzString )
if err != nil {
c . UI . Error ( fmt . Errorf ( "Unable to base58-decode authorization data: %w" , err ). Error ())
return 1
}
2020-09-12 21:43:08 -04:00
if len ( marshaled ) == 0 {
c . UI . Error ( "Zero length authorization information after decoding" )
2020-09-03 13:24:34 -04:00
return 1
}
2020-09-19 00:18:17 -04:00
data := new ( targetspb . SessionAuthorizationData )
if err := proto . Unmarshal ( marshaled , data ); err != nil {
c . UI . Error ( fmt . Errorf ( "Unable to proto-decode authorization data: %w" , err ). Error ())
2020-09-03 13:24:34 -04:00
return 1
}
2020-09-19 00:18:17 -04:00
if len ( data . GetWorkerInfo ()) == 0 {
2020-09-03 13:24:34 -04:00
c . UI . Error ( "No workers found in authorization string" )
return 1
}
2020-09-20 21:18:07 -04:00
c . connectionsLeft . Store ( data . ConnectionLimit )
2020-09-20 12:51:08 -04:00
workerAddr := data . GetWorkerInfo ()[ 0 ]. GetAddress ()
2020-09-19 00:18:17 -04:00
parsedCert , err := x509 . ParseCertificate ( data . Certificate )
2020-09-03 13:24:34 -04:00
if err != nil {
c . UI . Error ( fmt . Errorf ( "Unable to decode mTLS certificate: %w" , err ). Error ())
return 1
}
if len ( parsedCert . DNSNames ) != 1 {
c . UI . Error ( fmt . Errorf ( "mTLS certificate has invalid parameters: %w" , err ). Error ())
return 1
}
2020-09-20 21:18:07 -04:00
c . expiration = parsedCert . NotAfter
2020-09-20 16:28:00 -04:00
2020-09-24 17:29:12 -04:00
// We don't _rely_ on client-side timeout verification but this prevents us
// seeming to be ready for a connection that will immediately fail when we
// try to actually make it
c . proxyCtx , c . proxyCancel = context . WithDeadline ( c . Context , c . expiration )
defer c . proxyCancel ()
2020-09-03 13:24:34 -04:00
certPool := x509 . NewCertPool ()
certPool . AddCert ( parsedCert )
tlsConf := & tls . Config {
Certificates : [] tls . Certificate {
{
2020-09-19 00:18:17 -04:00
Certificate : [][] byte { data . Certificate },
PrivateKey : ed25519 . PrivateKey ( data . PrivateKey ),
2020-09-03 13:24:34 -04:00
Leaf : parsedCert ,
},
},
RootCAs : certPool ,
ServerName : parsedCert . DNSNames [ 0 ],
MinVersion : tls . VersionTLS13 ,
}
transport := cleanhttp . DefaultTransport ()
transport . DisableKeepAlives = false
transport . TLSClientConfig = tlsConf
2020-09-19 14:01:49 -04:00
// This isn't/shouldn't used anyways really because the connection is
// hijacked, just setting for completeness
2020-09-19 13:51:50 -04:00
transport . IdleConnTimeout = 0
2020-09-03 13:24:34 -04:00
2020-09-20 21:18:07 -04:00
c . listener , err = net . ListenTCP ( "tcp" , & net . TCPAddr {
2020-09-03 13:24:34 -04:00
IP : listenAddr ,
Port : c . flagListenPort ,
})
if err != nil {
c . UI . Error ( fmt . Errorf ( "Error starting listening port: %w" , err ). Error ())
return 1
}
2020-09-20 12:51:08 -04:00
listenerCloseFunc := func () {
2020-09-20 21:18:07 -04:00
// Forces the for loop to exist instead of spinning on errors
c . connectionsLeft . Store ( 0 )
if err := c . listener . Close (); err != nil {
2020-09-20 12:51:08 -04:00
c . UI . Error ( fmt . Errorf ( "Error closing listener on shutdown: %w" , err ). Error ())
retCode = 1
}
}
// Ensure it runs on any other return condition
defer func () {
2020-09-20 21:18:07 -04:00
c . listenerCloseOnce . Do ( listenerCloseFunc )
2020-09-20 12:51:08 -04:00
}()
2020-09-24 17:29:12 -04:00
c . listenerAddr = c . listener . Addr ().( * net . TCPAddr )
2020-09-20 12:51:08 -04:00
2020-09-24 17:29:12 -04:00
if c . flagExec == "" {
sessInfo := SessionInfo {
Protocol : "tcp" ,
Address : c . listenerAddr . IP . String (),
Port : c . listenerAddr . Port ,
Expiration : c . expiration ,
ConnectionLimit : data . GetConnectionLimit (),
SessionId : data . GetSessionId (),
}
2020-09-20 12:51:08 -04:00
2020-09-24 17:29:12 -04:00
switch base . Format ( c . UI ) {
case "table" :
c . UI . Output ( generateSessionInfoTableOutput ( sessInfo ))
case "json" :
out , err := json . Marshal ( & sessInfo )
if err != nil {
c . UI . Error ( fmt . Errorf ( "error marshaling session information: %w" , err ). Error ())
return 1
}
c . UI . Output ( string ( out ))
2020-09-20 12:51:08 -04:00
}
}
2020-09-20 21:18:07 -04:00
c . connWg = new ( sync . WaitGroup )
2020-09-20 12:51:08 -04:00
2020-09-20 21:18:07 -04:00
c . connWg . Add ( 1 )
go func () {
defer c . connWg . Done ()
for {
listeningConn , err := c . listener . AcceptTCP ()
if err != nil {
select {
2020-09-24 17:29:12 -04:00
case <- c . proxyCtx . Done ():
return
2020-09-20 21:18:07 -04:00
case <- c . Context . Done ():
return
default :
// When this hits zero we tigger listener close so this
// isn't actually an error condition
if c . connectionsLeft . Load () == 0 {
return
}
c . UI . Error ( fmt . Errorf ( "Error accepting connection: %w" , err ). Error ())
continue
}
2020-09-20 12:51:08 -04:00
}
2020-09-20 21:18:07 -04:00
c . connWg . Add ( 1 )
go func () {
defer listeningConn . Close ()
if err := c . handleConnection (
listeningConn ,
workerAddr ,
tofuToken ,
transport ); err != nil {
c . UI . Error ( err . Error ())
}
}()
2020-09-20 12:51:08 -04:00
}
2020-09-20 21:18:07 -04:00
}()
timer := time . NewTimer ( time . Until ( c . expiration ))
2020-09-24 17:29:12 -04:00
c . connWg . Add ( 1 )
go func () {
defer c . connWg . Done ()
defer c . listenerCloseOnce . Do ( listenerCloseFunc )
for {
select {
case <- c . proxyCtx . Done ():
timer . Stop ()
return
case <- c . Context . Done ():
timer . Stop ()
return
case <- timer . C :
return
case connsLeft := <- c . connsLeftCh :
c . updateConnsLeft ( connsLeft )
if connsLeft == 0 {
return
}
2020-09-20 12:51:08 -04:00
}
2020-09-20 21:18:07 -04:00
}
2020-09-24 17:29:12 -04:00
}()
if c . flagExec != "" {
c . connWg . Add ( 1 )
c . execCmdReturnValue = new ( atomic . Int32 )
go c . handleExec ( passthroughArgs )
2020-09-20 21:18:07 -04:00
}
c . connWg . Wait ()
2020-09-24 17:29:12 -04:00
if c . execCmdReturnValue != nil {
retCode = int ( c . execCmdReturnValue . Load ())
}
termInfo := TerminationInfo { Reason : "Unknown" }
select {
case <- c . Context . Done ():
termInfo . Reason = "Received shutdown signal"
case <- timer . C :
termInfo . Reason = "Session has expired"
default :
if c . execCmdReturnValue != nil {
2020-10-01 17:13:47 -04:00
// Don't print out in this case, so ensure we clear it
termInfo . Reason = ""
2020-09-24 17:29:12 -04:00
} else {
if c . connectionsLeft . Load () == 0 {
termInfo . Reason = "No connections left in session"
}
}
}
2020-10-01 00:27:13 -04:00
if termInfo . Reason != "" {
switch base . Format ( c . UI ) {
case "table" :
c . UI . Output ( generateTerminationInfoTableOutput ( termInfo ))
case "json" :
out , err := json . Marshal ( & termInfo )
if err != nil {
c . UI . Error ( fmt . Errorf ( "error marshaling termination information: %w" , err ). Error ())
return 1
}
c . UI . Output ( string ( out ))
2020-09-20 21:18:07 -04:00
}
2020-09-20 12:51:08 -04:00
}
2020-09-24 17:29:12 -04:00
return
2020-09-20 12:51:08 -04:00
}
2020-09-20 21:18:07 -04:00
func ( c * Command ) handleConnection (
2020-09-20 12:51:08 -04:00
listeningConn * net . TCPConn ,
workerAddr string ,
tofuToken string ,
transport * http . Transport ) error {
2020-09-20 21:18:07 -04:00
defer c . connWg . Done ()
2020-09-03 13:24:34 -04:00
conn , resp , err := websocket . Dial (
2020-09-24 17:29:12 -04:00
c . proxyCtx ,
2020-09-03 13:24:34 -04:00
fmt . Sprintf ( "wss://%s/v1/proxy" , workerAddr ),
& websocket . DialOptions {
HTTPClient : & http . Client {
Transport : transport ,
},
Subprotocols : [] string { globals . TcpProxyV1 },
},
)
if err != nil {
switch {
case strings . Contains ( err . Error (), "tls: internal error" ):
2020-09-20 12:51:08 -04:00
return errors . New ( "Session is unauthorized" )
2020-09-03 13:24:34 -04:00
case strings . Contains ( err . Error (), "connect: connection refused" ):
2020-09-20 12:51:08 -04:00
return fmt . Errorf ( "Unable to connect to worker at %s" , workerAddr )
2020-09-03 13:24:34 -04:00
default :
2020-09-20 12:51:08 -04:00
return fmt . Errorf ( "Error dialing the worker: %w" , err )
2020-09-03 13:24:34 -04:00
}
}
if resp == nil {
2020-09-20 12:51:08 -04:00
return errors . New ( "Response from worker is nil" )
2020-09-03 13:24:34 -04:00
}
if resp . Header == nil {
2020-09-20 12:51:08 -04:00
return errors . New ( "Response header is nil" )
2020-09-03 13:24:34 -04:00
}
negProto := resp . Header . Get ( "Sec-WebSocket-Protocol" )
if negProto != globals . TcpProxyV1 {
2020-09-20 12:51:08 -04:00
return fmt . Errorf ( "Unexpected negotiated protocol: %s" , negProto )
2020-09-03 13:24:34 -04:00
}
2020-09-20 12:51:08 -04:00
handshake := proxy . ClientHandshake { TofuToken : tofuToken }
2020-09-24 17:29:12 -04:00
if err := wspb . Write ( c . proxyCtx , conn , & handshake ); err != nil {
2020-09-20 12:51:08 -04:00
return fmt . Errorf ( "error sending handshake to worker: %w" , err )
2020-09-10 23:25:40 -04:00
}
2020-09-19 00:18:17 -04:00
var handshakeResult proxy . HandshakeResult
2020-09-24 17:29:12 -04:00
if err := wspb . Read ( c . proxyCtx , conn , & handshakeResult ); err != nil {
2020-09-21 02:22:59 -04:00
switch {
case strings . Contains ( err . Error (), "unable to authorize connection" ):
// There's no reason to think we'd be able to authorize any more
// connections after the first has failed
c . connsLeftCh <- 0
return errors . New ( "Unable to authorize connection" )
}
2020-09-20 12:51:08 -04:00
return fmt . Errorf ( "error reading handshake result: %w" , err )
2020-09-19 00:18:17 -04:00
}
2020-09-20 16:28:00 -04:00
2020-09-20 21:18:07 -04:00
if handshakeResult . GetConnectionsLeft () != - 1 {
c . connsLeftCh <- handshakeResult . GetConnectionsLeft ()
2020-09-20 16:28:00 -04:00
}
2020-09-10 23:25:40 -04:00
2020-09-03 13:24:34 -04:00
// Get a wrapped net.Conn so we can use io.Copy
2020-09-24 17:29:12 -04:00
netConn := websocket . NetConn ( c . proxyCtx , conn , websocket . MessageBinary )
2020-09-03 13:24:34 -04:00
2020-09-20 12:51:08 -04:00
localWg := new ( sync . WaitGroup )
localWg . Add ( 2 )
2020-09-03 13:24:34 -04:00
go func () {
2020-09-20 12:51:08 -04:00
defer localWg . Done ()
io . Copy ( netConn , listeningConn )
2020-09-03 13:24:34 -04:00
netConn . Close ()
listeningConn . Close ()
}()
go func () {
2020-09-20 12:51:08 -04:00
defer localWg . Done ()
io . Copy ( listeningConn , netConn )
2020-09-03 13:24:34 -04:00
listeningConn . Close ()
netConn . Close ()
}()
2020-09-20 12:51:08 -04:00
localWg . Wait ()
return nil
2020-09-03 13:24:34 -04:00
}
2020-09-20 21:18:07 -04:00
func ( c * Command ) updateConnsLeft ( connsLeft int32 ) {
c . connectionsLeft . Store ( connsLeft )
connInfo := ConnectionInfo {
ConnectionsLeft : connsLeft ,
}
2020-09-24 17:29:12 -04:00
if c . flagExec == "" {
switch base . Format ( c . UI ) {
case "table" :
c . UI . Output ( generateConnectionInfoTableOutput ( connInfo ))
case "json" :
out , err := json . Marshal ( & connInfo )
if err != nil {
c . UI . Error ( fmt . Errorf ( "error marshaling connection information: %w" , err ). Error ())
}
c . UI . Output ( string ( out ))
2020-09-20 21:18:07 -04:00
}
}
}
2020-09-24 17:29:12 -04:00
func ( c * Command ) handleExec ( passthroughArgs [] string ) {
defer c . connWg . Done ()
defer c . proxyCancel ()
port := strconv . Itoa ( c . listenerAddr . Port )
ip := c . listenerAddr . IP . String ()
addr := c . listenerAddr . String ()
var args [] string
switch c . Func {
2020-10-01 00:27:13 -04:00
case "http" :
switch c . flagHttpStyle {
case "curl" :
if c . flagHttpMethod != "" {
args = append ( args , "-X" , c . flagHttpMethod )
}
2020-10-01 12:16:18 -04:00
var uri string
2020-10-01 00:27:13 -04:00
if c . flagHttpHost != "" {
2020-10-01 12:19:06 -04:00
c . flagHttpHost = strings . TrimSuffix ( c . flagHttpHost , "/" )
2020-10-01 00:27:13 -04:00
args = append ( args , "-H" , fmt . Sprintf ( "Host: %s" , c . flagHttpHost ))
args = append ( args , "--resolve" , fmt . Sprintf ( "%s:%s:%s" , c . flagHttpHost , port , ip ))
2020-10-01 12:16:18 -04:00
uri = fmt . Sprintf ( "%s://%s:%s" , c . flagHttpScheme , c . flagHttpHost , port )
2020-10-01 00:27:13 -04:00
} else {
2020-10-01 12:16:18 -04:00
uri = fmt . Sprintf ( "%s://%s" , c . flagHttpScheme , addr )
}
if c . flagHttpPath != "" {
uri = fmt . Sprintf ( "%s/%s" , uri , strings . TrimPrefix ( c . flagHttpPath , "/" ))
2020-10-01 00:27:13 -04:00
}
2020-10-01 12:16:18 -04:00
args = append ( args , uri )
2020-10-01 00:27:13 -04:00
}
2020-09-24 17:29:12 -04:00
case "ssh" :
switch c . flagSshStyle {
case "ssh" :
args = append ( args , "-p" , port , ip )
2020-10-01 17:07:34 -04:00
args = append ( args , "-o" , fmt . Sprintf ( "HostKeyAlias=%s" , c . sessionAuthz . HostId ))
2020-09-24 17:29:12 -04:00
case "putty" :
args = append ( args , "-P" , port , ip )
}
2020-09-25 12:26:16 -04:00
if c . flagUsername != "" {
args = append ( args , "-l" , c . flagUsername )
}
2020-09-24 21:16:12 -04:00
case "postgres" :
switch c . flagPostgresStyle {
case "psql" :
args = append ( args , "-p" , port , "-h" , ip )
2020-09-25 12:26:16 -04:00
if c . flagUsername != "" {
args = append ( args , "-U" , c . flagUsername )
2020-09-24 21:16:12 -04:00
}
}
case "rdp" :
switch c . flagRdpStyle {
case "mstsc" :
args = append ( args , "/v" , addr )
}
2020-09-24 17:29:12 -04:00
}
args = append ( passthroughArgs , args ... )
// Might want -t for ssh or -tt but seems fine without it for now...
stringReplacer := func ( in , typ , replacer string ) string {
for _ , style := range [] string {
fmt . Sprintf ( "{{boundary.%s}}" , typ ),
fmt . Sprintf ( "{{ boundary.%s}}" , typ ),
fmt . Sprintf ( "{{boundary.%s }}" , typ ),
fmt . Sprintf ( "{{ boundary.%s }}" , typ ),
} {
in = strings . Replace ( in , style , replacer , - 1 )
}
return in
}
for i := range args {
args [ i ] = stringReplacer ( args [ i ], "port" , port )
args [ i ] = stringReplacer ( args [ i ], "ip" , ip )
args [ i ] = stringReplacer ( args [ i ], "addr" , addr )
}
// NOTE: exec.CommandContext is a hard kill, so if used it leaves the
// terminal in a weird state. It suffices to simply close the connection,
// which already happens, so we don't need/want CommandContext here.
cmd := exec . Command ( c . flagExec , args ... )
cmd . Env = append ( os . Environ (),
fmt . Sprintf ( "BOUNDARY_PROXIED_PORT=%s" , port ),
fmt . Sprintf ( "BOUNDARY_PROXIED_IP=%s" , ip ),
fmt . Sprintf ( "BOUNDARY_PROXIED_ADDR=%s" , addr ),
)
cmd . Stdin = os . Stdin
cmd . Stdout = os . Stdout
cmd . Stderr = os . Stderr
if err := cmd . Run (); err != nil {
exitCode := 2
if exitError , ok := err .( * exec . ExitError ); ok {
if exitError . Success () {
c . execCmdReturnValue . Store ( 0 )
return
}
if ws , ok := exitError . Sys ().( syscall . WaitStatus ); ok {
c . execCmdReturnValue . Store ( int32 ( ws . ExitStatus ()))
return
}
}
c . UI . Error ( fmt . Sprintf ( "Failed to run command: %s" , err ))
c . execCmdReturnValue . Store ( int32 ( exitCode ))
return
}
c . execCmdReturnValue . Store ( 0 )
}