From b89cc22896c098f78de2647a6efe767825d8ccd0 Mon Sep 17 00:00:00 2001 From: Michael Borkenstein Date: Mon, 4 May 2020 15:15:17 -0500 Subject: [PATCH] AUTH-2369: RDP Bastion prototype --- carrier/carrier.go | 3 +- cmd/cloudflared/access/carrier.go | 7 ++-- cmd/cloudflared/access/cmd.go | 7 ++-- cmd/cloudflared/tunnel/cmd.go | 44 +++++++++++++++++++++---- cmd/cloudflared/tunnel/configuration.go | 9 ++--- h2mux/header.go | 5 +++ sshserver/sshserver_unix.go | 1 + websocket/websocket.go | 35 +++++++++++--------- 8 files changed, 78 insertions(+), 33 deletions(-) diff --git a/carrier/carrier.go b/carrier/carrier.go index 1bdfeb95..b36a0e51 100644 --- a/carrier/carrier.go +++ b/carrier/carrier.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/cloudflare/cloudflared/cmd/cloudflared/token" + "github.com/cloudflare/cloudflared/h2mux" "github.com/pkg/errors" ) @@ -145,7 +146,7 @@ func BuildAccessRequest(options *StartOptions) (*http.Request, error) { if err != nil { return nil, err } - originRequest.Header.Set("cf-access-token", token) + originRequest.Header.Set(h2mux.CFAccessTokenHeader, token) for k, v := range options.Headers { if len(v) >= 1 { diff --git a/cmd/cloudflared/access/carrier.go b/cmd/cloudflared/access/carrier.go index ddcffbf3..c258f85b 100644 --- a/cmd/cloudflared/access/carrier.go +++ b/cmd/cloudflared/access/carrier.go @@ -7,6 +7,7 @@ import ( "github.com/cloudflare/cloudflared/carrier" "github.com/cloudflare/cloudflared/cmd/cloudflared/config" + "github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/validation" "github.com/pkg/errors" cli "gopkg.in/urfave/cli.v2" @@ -54,15 +55,15 @@ func ssh(c *cli.Context) error { // get the headers from the cmdline and add them headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag)) if c.IsSet(sshTokenIDFlag) { - headers.Add("CF-Access-Client-Id", c.String(sshTokenIDFlag)) + headers.Add(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag)) } if c.IsSet(sshTokenSecretFlag) { - headers.Add("CF-Access-Client-Secret", c.String(sshTokenSecretFlag)) + headers.Add(h2mux.CFAccessClientSecretHeader, c.String(sshTokenSecretFlag)) } destination := c.String(sshDestinationFlag) if destination != "" { - headers.Add("CF-Access-SSH-Destination", destination) + headers.Add(h2mux.CFJumpDestinationHeader, destination) } options := &carrier.StartOptions{ diff --git a/cmd/cloudflared/access/cmd.go b/cmd/cloudflared/access/cmd.go index 72bd67d7..7f4f1abb 100644 --- a/cmd/cloudflared/access/cmd.go +++ b/cmd/cloudflared/access/cmd.go @@ -13,6 +13,7 @@ import ( "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/shell" "github.com/cloudflare/cloudflared/cmd/cloudflared/token" + "github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/sshgen" "github.com/cloudflare/cloudflared/validation" "github.com/pkg/errors" @@ -262,7 +263,7 @@ func curl(c *cli.Context) error { } cmdArgs = append(cmdArgs, "-H") - cmdArgs = append(cmdArgs, fmt.Sprintf("cf-access-token: %s", tok)) + cmdArgs = append(cmdArgs, fmt.Sprintf("%s: %s", h2mux.CFAccessTokenHeader, tok)) return shell.Run("curl", cmdArgs...) } @@ -415,10 +416,10 @@ func isFileThere(candidate string) bool { func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context) error { headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag)) if c.IsSet(sshTokenIDFlag) { - headers.Add("CF-Access-Client-Id", c.String(sshTokenIDFlag)) + headers.Add(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag)) } if c.IsSet(sshTokenSecretFlag) { - headers.Add("CF-Access-Client-Secret", c.String(sshTokenSecretFlag)) + headers.Add(h2mux.CFAccessClientSecretHeader, c.String(sshTokenSecretFlag)) } options := &carrier.StartOptions{OriginURL: appUrl.String(), Headers: headers} diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go index b3576d51..843462ba 100644 --- a/cmd/cloudflared/tunnel/cmd.go +++ b/cmd/cloudflared/tunnel/cmd.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "net" + "net/http" "net/url" "os" "reflect" @@ -22,6 +23,7 @@ import ( "github.com/cloudflare/cloudflared/cmd/cloudflared/updater" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/dbconnect" + "github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/hello" "github.com/cloudflare/cloudflared/metrics" "github.com/cloudflare/cloudflared/origin" @@ -81,9 +83,15 @@ const ( // hostKeyPath is the path of the dir to save SSH host keys too hostKeyPath = "host-key-path" + //sshServerFlag enables cloudflared ssh proxy server + sshServerFlag = "ssh-server" + // socks5Flag is to enable the socks server to deframe socks5Flag = "socks5" + // bastionFlag is to enable bastion, or jump host, operation + bastionFlag = "bastion" + noIntentMsg = "The --intent argument is required. Cloudflared looks up an Intent to determine what configuration to use (i.e. which tunnels to start). If you don't have any Intents yet, you can use a placeholder Intent Label for now. Then, when you make an Intent with that label, cloudflared will get notified and open the tunnels you specified in that Intent." ) @@ -343,12 +351,11 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan c.Set("url", "https://"+helloListener.Addr().String()) } - if c.IsSet("ssh-server") { + if c.IsSet(sshServerFlag) { if runtime.GOOS != "darwin" && runtime.GOOS != "linux" { msg := fmt.Sprintf("--ssh-server is not supported on %s", runtime.GOOS) logger.Error(msg) return errors.New(msg) - } logger.Infof("ssh-server set") @@ -394,7 +401,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan c.Set("url", "ssh://"+localServerAddress) } - if host := hostnameFromURI(c.String("url")); host != "" { + if staticHost := hostnameFromURI(c.String("url")); isProxyDestinationConfigured(staticHost, c) { listener, err := net.Listen("tcp", "127.0.0.1:") if err != nil { logger.WithError(err).Error("Cannot start Websocket Proxy Server") @@ -406,15 +413,26 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan streamHandler := websocket.DefaultStreamHandler if c.IsSet(socks5Flag) { logger.Info("SOCKS5 server started") - streamHandler = func(wsConn *websocket.Conn, remoteConn net.Conn) { + streamHandler = func(wsConn *websocket.Conn, remoteConn net.Conn, _ http.Header) { dialer := socks.NewConnDialer(remoteConn) requestHandler := socks.NewRequestHandler(dialer) socksServer := socks.NewConnectionHandler(requestHandler) socksServer.Serve(wsConn) } + } else if c.IsSet(sshServerFlag) { + streamHandler = func(wsConn *websocket.Conn, remoteConn net.Conn, requestHeaders http.Header) { + if finalDestination := requestHeaders.Get(h2mux.CFJumpDestinationHeader); finalDestination != "" { + token := requestHeaders.Get(h2mux.CFAccessTokenHeader) + if err := websocket.SendSSHPreamble(remoteConn, finalDestination, token); err != nil { + logger.WithError(err).Error("Failed to send SSH preamble") + return + } + } + websocket.DefaultStreamHandler(wsConn, remoteConn, requestHeaders) + } } - errC <- websocket.StartProxyServer(logger, listener, host, shutdownC, streamHandler) + errC <- websocket.StartProxyServer(logger, listener, staticHost, shutdownC, streamHandler) }() c.Set("url", "http://"+listener.Addr().String()) } @@ -457,6 +475,11 @@ func Before(c *cli.Context) error { return nil } +// isProxyDestinationConfigured returns true if there is a static host set or if bastion mode is set. +func isProxyDestinationConfigured(staticHost string, c *cli.Context) bool { + return staticHost != "" || c.IsSet(bastionFlag) +} + func startDeclarativeTunnel(ctx context.Context, c *cli.Context, cloudflaredID uuid.UUID, @@ -882,7 +905,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag { Hidden: shouldHide, }), altsrc.NewBoolFlag(&cli.BoolFlag{ - Name: "ssh-server", + Name: sshServerFlag, Value: false, Usage: "Run an SSH Server", EnvVars: []string{"TUNNEL_SSH_SERVER"}, @@ -1118,7 +1141,14 @@ func tunnelFlags(shouldHide bool) []cli.Flag { Usage: "specify if this tunnel is running as a SOCK5 Server", EnvVars: []string{"TUNNEL_SOCKS"}, Value: false, - Hidden: false, + Hidden: shouldHide, + }), + altsrc.NewBoolFlag(&cli.BoolFlag{ + Name: bastionFlag, + Value: false, + Usage: "Runs as jump host", + EnvVars: []string{"TUNNEL_BASTION"}, + Hidden: shouldHide, }), } } diff --git a/cmd/cloudflared/tunnel/configuration.go b/cmd/cloudflared/tunnel/configuration.go index 8cd15d6c..39772845 100644 --- a/cmd/cloudflared/tunnel/configuration.go +++ b/cmd/cloudflared/tunnel/configuration.go @@ -233,10 +233,11 @@ func prepareTunnelConfig( if !c.IsSet("hello-world") && c.IsSet("origin-server-name") { httpTransport.TLSClientConfig.ServerName = c.String("origin-server-name") } - - err = validation.ValidateHTTPService(originURL, hostname, httpTransport) - if err != nil { - logger.WithError(err).Error("unable to connect to the origin") + // If tunnel running in bastion mode, a connection to origin will not exist until initiated by the client. + if !c.IsSet(bastionFlag) { + if err = validation.ValidateHTTPService(originURL, hostname, httpTransport); err != nil { + logger.WithError(err).Error("unable to connect to the origin") + } } toEdgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c) diff --git a/h2mux/header.go b/h2mux/header.go index 5490b050..58d418e2 100644 --- a/h2mux/header.go +++ b/h2mux/header.go @@ -25,6 +25,11 @@ const ( ResponseMetaHeaderField = "cf-cloudflared-response-meta" ResponseSourceCloudflared = "cloudflared" ResponseSourceOrigin = "origin" + + CFAccessTokenHeader = "cf-access-token" + CFJumpDestinationHeader = "CF-Access-Jump-Destination" + CFAccessClientIDHeader = "CF-Access-Client-Id" + CFAccessClientSecretHeader = "CF-Access-Client-Secret" ) // H2RequestHeadersToH1Request converts the HTTP/2 headers coming from origintunneld diff --git a/sshserver/sshserver_unix.go b/sshserver/sshserver_unix.go index d4552832..8a5b9b2d 100644 --- a/sshserver/sshserver_unix.go +++ b/sshserver/sshserver_unix.go @@ -267,6 +267,7 @@ func (s *SSHProxy) proxyChannel(localChan, remoteChan gossh.Channel, localChanRe } } + // readPreamble reads a preamble from the SSH connection before any SSH traffic is sent. // This preamble is a JSON encoded struct containing the users JWT and ultimate destination. // The first 4 bytes contain the length of the preamble which follows immediately. diff --git a/websocket/websocket.go b/websocket/websocket.go index 823879aa..31d7f3d8 100644 --- a/websocket/websocket.go +++ b/websocket/websocket.go @@ -13,6 +13,7 @@ import ( "net/http" "time" + "github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/sshserver" "github.com/gorilla/websocket" "github.com/sirupsen/logrus" @@ -118,13 +119,13 @@ func Stream(conn, backendConn io.ReadWriter) { // DefaultStreamHandler is provided to the the standard websocket to origin stream // This exist to allow SOCKS to deframe data before it gets to the origin -func DefaultStreamHandler(wsConn *Conn, remoteConn net.Conn) { +func DefaultStreamHandler(wsConn *Conn, remoteConn net.Conn, _ http.Header) { Stream(wsConn, remoteConn) } // StartProxyServer will start a websocket server that will decode // the websocket data and write the resulting data to the provided -func StartProxyServer(logger *logrus.Logger, listener net.Listener, remote string, shutdownC <-chan struct{}, streamHandler func(wsConn *Conn, remoteConn net.Conn)) error { +func StartProxyServer(logger *logrus.Logger, listener net.Listener, staticHost string, shutdownC <-chan struct{}, streamHandler func(wsConn *Conn, remoteConn net.Conn, requestHeaders http.Header)) error { upgrader := websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, @@ -137,7 +138,18 @@ func StartProxyServer(logger *logrus.Logger, listener net.Listener, remote strin }() http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - stream, err := net.Dial("tcp", remote) + // If remote is an empty string, get the destination from the client. + finalDestination := staticHost + if finalDestination == "" { + if jumpDestination := r.Header.Get(h2mux.CFJumpDestinationHeader); jumpDestination == "" { + logger.Error("Did not receive final destination from client. The --destination flag is likely not set") + return + } else { + finalDestination = jumpDestination + } + } + + stream, err := net.Dial("tcp", finalDestination) if err != nil { logger.WithError(err).Error("Cannot connect to remote.") return @@ -162,24 +174,17 @@ func StartProxyServer(logger *logrus.Logger, listener net.Listener, remote strin conn.Close() }() - token := r.Header.Get("cf-access-token") - if destination := r.Header.Get("CF-Access-SSH-Destination"); destination != "" { - if err := sendSSHPreamble(stream, destination, token); err != nil { - logger.WithError(err).Error("Failed to send SSH preamble") - return - } - } - - streamHandler(&Conn{conn}, stream) + streamHandler(&Conn{conn}, stream, r.Header) }) return httpServer.Serve(listener) } -// sendSSHPreamble sends the final SSH destination address to the cloudflared SSH proxy +// SendSSHPreamble sends the final SSH destination address to the cloudflared SSH proxy // The destination is preceded by its length -func sendSSHPreamble(stream net.Conn, destination, token string) error { - preamble := &sshserver.SSHPreamble{Destination: destination, JWT: token} +// Not part of sshserver module to fix compilation for incompatible operating systems +func SendSSHPreamble(stream net.Conn, destination, token string) error { + preamble := sshserver.SSHPreamble{Destination: destination, JWT: token} payload, err := json.Marshal(preamble) if err != nil { return err