AUTH-2369: RDP Bastion prototype
This commit is contained in:
parent
6a7418e1af
commit
b89cc22896
|
@ -11,6 +11,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||||
|
"github.com/cloudflare/cloudflared/h2mux"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -145,7 +146,7 @@ func BuildAccessRequest(options *StartOptions) (*http.Request, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
originRequest.Header.Set("cf-access-token", token)
|
originRequest.Header.Set(h2mux.CFAccessTokenHeader, token)
|
||||||
|
|
||||||
for k, v := range options.Headers {
|
for k, v := range options.Headers {
|
||||||
if len(v) >= 1 {
|
if len(v) >= 1 {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/carrier"
|
"github.com/cloudflare/cloudflared/carrier"
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||||
|
"github.com/cloudflare/cloudflared/h2mux"
|
||||||
"github.com/cloudflare/cloudflared/validation"
|
"github.com/cloudflare/cloudflared/validation"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
cli "gopkg.in/urfave/cli.v2"
|
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
|
// get the headers from the cmdline and add them
|
||||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||||
if c.IsSet(sshTokenIDFlag) {
|
if c.IsSet(sshTokenIDFlag) {
|
||||||
headers.Add("CF-Access-Client-Id", c.String(sshTokenIDFlag))
|
headers.Add(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag))
|
||||||
}
|
}
|
||||||
if c.IsSet(sshTokenSecretFlag) {
|
if c.IsSet(sshTokenSecretFlag) {
|
||||||
headers.Add("CF-Access-Client-Secret", c.String(sshTokenSecretFlag))
|
headers.Add(h2mux.CFAccessClientSecretHeader, c.String(sshTokenSecretFlag))
|
||||||
}
|
}
|
||||||
|
|
||||||
destination := c.String(sshDestinationFlag)
|
destination := c.String(sshDestinationFlag)
|
||||||
if destination != "" {
|
if destination != "" {
|
||||||
headers.Add("CF-Access-SSH-Destination", destination)
|
headers.Add(h2mux.CFJumpDestinationHeader, destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
options := &carrier.StartOptions{
|
options := &carrier.StartOptions{
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||||
|
"github.com/cloudflare/cloudflared/h2mux"
|
||||||
"github.com/cloudflare/cloudflared/sshgen"
|
"github.com/cloudflare/cloudflared/sshgen"
|
||||||
"github.com/cloudflare/cloudflared/validation"
|
"github.com/cloudflare/cloudflared/validation"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -262,7 +263,7 @@ func curl(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdArgs = append(cmdArgs, "-H")
|
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...)
|
return shell.Run("curl", cmdArgs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -415,10 +416,10 @@ func isFileThere(candidate string) bool {
|
||||||
func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context) error {
|
func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context) error {
|
||||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||||
if c.IsSet(sshTokenIDFlag) {
|
if c.IsSet(sshTokenIDFlag) {
|
||||||
headers.Add("CF-Access-Client-Id", c.String(sshTokenIDFlag))
|
headers.Add(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag))
|
||||||
}
|
}
|
||||||
if c.IsSet(sshTokenSecretFlag) {
|
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}
|
options := &carrier.StartOptions{OriginURL: appUrl.String(), Headers: headers}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
@ -22,6 +23,7 @@ import (
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||||
"github.com/cloudflare/cloudflared/connection"
|
"github.com/cloudflare/cloudflared/connection"
|
||||||
"github.com/cloudflare/cloudflared/dbconnect"
|
"github.com/cloudflare/cloudflared/dbconnect"
|
||||||
|
"github.com/cloudflare/cloudflared/h2mux"
|
||||||
"github.com/cloudflare/cloudflared/hello"
|
"github.com/cloudflare/cloudflared/hello"
|
||||||
"github.com/cloudflare/cloudflared/metrics"
|
"github.com/cloudflare/cloudflared/metrics"
|
||||||
"github.com/cloudflare/cloudflared/origin"
|
"github.com/cloudflare/cloudflared/origin"
|
||||||
|
@ -81,9 +83,15 @@ const (
|
||||||
// hostKeyPath is the path of the dir to save SSH host keys too
|
// hostKeyPath is the path of the dir to save SSH host keys too
|
||||||
hostKeyPath = "host-key-path"
|
hostKeyPath = "host-key-path"
|
||||||
|
|
||||||
|
//sshServerFlag enables cloudflared ssh proxy server
|
||||||
|
sshServerFlag = "ssh-server"
|
||||||
|
|
||||||
// socks5Flag is to enable the socks server to deframe
|
// socks5Flag is to enable the socks server to deframe
|
||||||
socks5Flag = "socks5"
|
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."
|
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())
|
c.Set("url", "https://"+helloListener.Addr().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.IsSet("ssh-server") {
|
if c.IsSet(sshServerFlag) {
|
||||||
if runtime.GOOS != "darwin" && runtime.GOOS != "linux" {
|
if runtime.GOOS != "darwin" && runtime.GOOS != "linux" {
|
||||||
msg := fmt.Sprintf("--ssh-server is not supported on %s", runtime.GOOS)
|
msg := fmt.Sprintf("--ssh-server is not supported on %s", runtime.GOOS)
|
||||||
logger.Error(msg)
|
logger.Error(msg)
|
||||||
return errors.New(msg)
|
return errors.New(msg)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("ssh-server set")
|
logger.Infof("ssh-server set")
|
||||||
|
@ -394,7 +401,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||||
c.Set("url", "ssh://"+localServerAddress)
|
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:")
|
listener, err := net.Listen("tcp", "127.0.0.1:")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WithError(err).Error("Cannot start Websocket Proxy Server")
|
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
|
streamHandler := websocket.DefaultStreamHandler
|
||||||
if c.IsSet(socks5Flag) {
|
if c.IsSet(socks5Flag) {
|
||||||
logger.Info("SOCKS5 server started")
|
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)
|
dialer := socks.NewConnDialer(remoteConn)
|
||||||
requestHandler := socks.NewRequestHandler(dialer)
|
requestHandler := socks.NewRequestHandler(dialer)
|
||||||
socksServer := socks.NewConnectionHandler(requestHandler)
|
socksServer := socks.NewConnectionHandler(requestHandler)
|
||||||
|
|
||||||
socksServer.Serve(wsConn)
|
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())
|
c.Set("url", "http://"+listener.Addr().String())
|
||||||
}
|
}
|
||||||
|
@ -457,6 +475,11 @@ func Before(c *cli.Context) error {
|
||||||
return nil
|
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,
|
func startDeclarativeTunnel(ctx context.Context,
|
||||||
c *cli.Context,
|
c *cli.Context,
|
||||||
cloudflaredID uuid.UUID,
|
cloudflaredID uuid.UUID,
|
||||||
|
@ -882,7 +905,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||||
Hidden: shouldHide,
|
Hidden: shouldHide,
|
||||||
}),
|
}),
|
||||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||||
Name: "ssh-server",
|
Name: sshServerFlag,
|
||||||
Value: false,
|
Value: false,
|
||||||
Usage: "Run an SSH Server",
|
Usage: "Run an SSH Server",
|
||||||
EnvVars: []string{"TUNNEL_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",
|
Usage: "specify if this tunnel is running as a SOCK5 Server",
|
||||||
EnvVars: []string{"TUNNEL_SOCKS"},
|
EnvVars: []string{"TUNNEL_SOCKS"},
|
||||||
Value: false,
|
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,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -233,10 +233,11 @@ func prepareTunnelConfig(
|
||||||
if !c.IsSet("hello-world") && c.IsSet("origin-server-name") {
|
if !c.IsSet("hello-world") && c.IsSet("origin-server-name") {
|
||||||
httpTransport.TLSClientConfig.ServerName = c.String("origin-server-name")
|
httpTransport.TLSClientConfig.ServerName = c.String("origin-server-name")
|
||||||
}
|
}
|
||||||
|
// If tunnel running in bastion mode, a connection to origin will not exist until initiated by the client.
|
||||||
err = validation.ValidateHTTPService(originURL, hostname, httpTransport)
|
if !c.IsSet(bastionFlag) {
|
||||||
if err != nil {
|
if err = validation.ValidateHTTPService(originURL, hostname, httpTransport); err != nil {
|
||||||
logger.WithError(err).Error("unable to connect to the origin")
|
logger.WithError(err).Error("unable to connect to the origin")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toEdgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c)
|
toEdgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c)
|
||||||
|
|
|
@ -25,6 +25,11 @@ const (
|
||||||
ResponseMetaHeaderField = "cf-cloudflared-response-meta"
|
ResponseMetaHeaderField = "cf-cloudflared-response-meta"
|
||||||
ResponseSourceCloudflared = "cloudflared"
|
ResponseSourceCloudflared = "cloudflared"
|
||||||
ResponseSourceOrigin = "origin"
|
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
|
// H2RequestHeadersToH1Request converts the HTTP/2 headers coming from origintunneld
|
||||||
|
|
|
@ -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.
|
// 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.
|
// 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.
|
// The first 4 bytes contain the length of the preamble which follows immediately.
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/h2mux"
|
||||||
"github.com/cloudflare/cloudflared/sshserver"
|
"github.com/cloudflare/cloudflared/sshserver"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/sirupsen/logrus"
|
"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
|
// 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
|
// 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)
|
Stream(wsConn, remoteConn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartProxyServer will start a websocket server that will decode
|
// StartProxyServer will start a websocket server that will decode
|
||||||
// the websocket data and write the resulting data to the provided
|
// 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{
|
upgrader := websocket.Upgrader{
|
||||||
ReadBufferSize: 1024,
|
ReadBufferSize: 1024,
|
||||||
WriteBufferSize: 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) {
|
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 {
|
if err != nil {
|
||||||
logger.WithError(err).Error("Cannot connect to remote.")
|
logger.WithError(err).Error("Cannot connect to remote.")
|
||||||
return
|
return
|
||||||
|
@ -162,24 +174,17 @@ func StartProxyServer(logger *logrus.Logger, listener net.Listener, remote strin
|
||||||
conn.Close()
|
conn.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
token := r.Header.Get("cf-access-token")
|
streamHandler(&Conn{conn}, stream, r.Header)
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return httpServer.Serve(listener)
|
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
|
// The destination is preceded by its length
|
||||||
func sendSSHPreamble(stream net.Conn, destination, token string) error {
|
// Not part of sshserver module to fix compilation for incompatible operating systems
|
||||||
preamble := &sshserver.SSHPreamble{Destination: destination, JWT: token}
|
func SendSSHPreamble(stream net.Conn, destination, token string) error {
|
||||||
|
preamble := sshserver.SSHPreamble{Destination: destination, JWT: token}
|
||||||
payload, err := json.Marshal(preamble)
|
payload, err := json.Marshal(preamble)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
Loading…
Reference in New Issue