mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
Add support for proxying TCP apps (#13455)
Add support for proxying tcp apps
This commit is contained in:
@@ -61,6 +61,10 @@ type Application interface {
|
||||
GetRewrite() *Rewrite
|
||||
// IsAWSConsole returns true if this app is AWS management console.
|
||||
IsAWSConsole() bool
|
||||
// IsTCP returns true if this app represents a TCP endpoint.
|
||||
IsTCP() bool
|
||||
// GetProtocol returns the application protocol.
|
||||
GetProtocol() string
|
||||
// GetAWSAccountID returns value of label containing AWS account ID on this app.
|
||||
GetAWSAccountID() string
|
||||
// GetAWSExternalID returns the AWS External ID configured for this app.
|
||||
@@ -236,6 +240,19 @@ func (a *AppV3) IsAWSConsole() bool {
|
||||
return strings.HasPrefix(a.Spec.URI, constants.AWSConsoleURL)
|
||||
}
|
||||
|
||||
// IsTCP returns true if this app represents a TCP endpoint.
|
||||
func (a *AppV3) IsTCP() bool {
|
||||
return strings.HasPrefix(a.Spec.URI, "tcp://")
|
||||
}
|
||||
|
||||
// GetProtocol returns the application protocol.
|
||||
func (a *AppV3) GetProtocol() string {
|
||||
if a.IsTCP() {
|
||||
return "TCP"
|
||||
}
|
||||
return "HTTP"
|
||||
}
|
||||
|
||||
// GetAWSAccountID returns value of label containing AWS account ID on this app.
|
||||
func (a *AppV3) GetAWSAccountID() string {
|
||||
return a.Metadata.Labels[constants.AWSAccountIDLabel]
|
||||
|
||||
+1088
-590
File diff suppressed because it is too large
Load Diff
@@ -1402,6 +1402,33 @@ message AppSessionStart {
|
||||
[ (gogoproto.nullable) = false, (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
|
||||
}
|
||||
|
||||
// AppSessionEnd is emitted when an application session ends.
|
||||
message AppSessionEnd {
|
||||
// Metadata is a common event metadata
|
||||
Metadata Metadata = 1
|
||||
[ (gogoproto.nullable) = false, (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
|
||||
|
||||
// User is a common user event metadata
|
||||
UserMetadata User = 2
|
||||
[ (gogoproto.nullable) = false, (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
|
||||
|
||||
// SessionMetadata is a common event session metadata
|
||||
SessionMetadata Session = 3
|
||||
[ (gogoproto.nullable) = false, (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
|
||||
|
||||
// ServerMetadata is a common server metadata
|
||||
ServerMetadata Server = 4
|
||||
[ (gogoproto.nullable) = false, (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
|
||||
|
||||
// ConnectionMetadata holds information about the connection
|
||||
ConnectionMetadata Connection = 5
|
||||
[ (gogoproto.nullable) = false, (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
|
||||
|
||||
// App is a common application resource metadata.
|
||||
AppMetadata App = 6
|
||||
[ (gogoproto.nullable) = false, (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
|
||||
}
|
||||
|
||||
// AppSessionChunk is emitted at the start of a 5 minute chunk on each
|
||||
// proxy. This chunk is used to buffer 5 minutes of audit events at a time
|
||||
// for applications.
|
||||
@@ -2029,6 +2056,7 @@ message OneOf {
|
||||
events.DatabaseSessionMalformedPacket DatabaseSessionMalformedPacket = 90;
|
||||
events.SFTP SFTP = 91;
|
||||
events.UpgradeWindowStartUpdate UpgradeWindowStartUpdate = 92;
|
||||
events.AppSessionEnd AppSessionEnd = 93;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,10 @@ func ToOneOf(in AuditEvent) (*OneOf, error) {
|
||||
out.Event = &OneOf_AppSessionStart{
|
||||
AppSessionStart: e,
|
||||
}
|
||||
case *AppSessionEnd:
|
||||
out.Event = &OneOf_AppSessionEnd{
|
||||
AppSessionEnd: e,
|
||||
}
|
||||
case *AppSessionChunk:
|
||||
out.Event = &OneOf_AppSessionChunk{
|
||||
AppSessionChunk: e,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2020-2021 Gravitational, Inc.
|
||||
Copyright 2020-2022 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@@ -33,9 +34,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/gravitational/teleport"
|
||||
"github.com/gravitational/teleport/api/breaker"
|
||||
apidefaults "github.com/gravitational/teleport/api/defaults"
|
||||
@@ -53,10 +51,14 @@ import (
|
||||
"github.com/gravitational/teleport/lib/jwt"
|
||||
"github.com/gravitational/teleport/lib/service"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/srv/alpnproxy"
|
||||
alpncommon "github.com/gravitational/teleport/lib/srv/alpnproxy/common"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
"github.com/gravitational/teleport/lib/web"
|
||||
"github.com/gravitational/teleport/lib/web/app"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/gravitational/oxy/forward"
|
||||
@@ -83,6 +85,7 @@ func TestAppAccess(t *testing.T) {
|
||||
t.Run("TestAppAccessNoHeaderOverrides", pack.appAccessNoHeaderOverrides)
|
||||
t.Run("TestAppAuditEvents", pack.appAuditEvents)
|
||||
t.Run("TestAppInvalidateAppSessionsOnLogout", pack.appInvalidateAppSessionsOnLogout)
|
||||
t.Run("TestAppAccessTCP", pack.appAccessTCP)
|
||||
|
||||
// This test should go last because it stops/starts app servers.
|
||||
t.Run("TestAppServersHA", pack.appServersHA)
|
||||
@@ -175,6 +178,43 @@ func (p *pack) appAccessWebsockets(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// appAccessTCP tests proxying of plain TCP applications through app access.
|
||||
func (p *pack) appAccessTCP(t *testing.T) {
|
||||
pack := setup(t)
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
address string
|
||||
outMessage string
|
||||
}{
|
||||
{
|
||||
description: "TCP app in root cluster",
|
||||
address: pack.startLocalProxy(t, pack.rootTCPPublicAddr, pack.rootAppClusterName),
|
||||
outMessage: pack.rootTCPMessage,
|
||||
},
|
||||
{
|
||||
description: "TCP app in leaf cluster",
|
||||
address: pack.startLocalProxy(t, pack.leafTCPPublicAddr, pack.leafAppClusterName),
|
||||
outMessage: pack.leafTCPMessage,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
conn, err := net.Dial("tcp", test.address)
|
||||
require.NoError(t, err)
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, err := conn.Read(buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp := strings.TrimSpace(string(buf[:n]))
|
||||
require.Equal(t, test.outMessage, resp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// appAccessClientCert tests mutual TLS authentication flow with application
|
||||
// access typically used in CLI by curl and other clients.
|
||||
func (p *pack) appAccessClientCert(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -711,6 +751,11 @@ type pack struct {
|
||||
rootWSSMessage string
|
||||
rootWSSAppURI string
|
||||
|
||||
rootTCPAppName string
|
||||
rootTCPPublicAddr string
|
||||
rootTCPMessage string
|
||||
rootTCPAppURI string
|
||||
|
||||
jwtAppName string
|
||||
jwtAppPublicAddr string
|
||||
jwtAppClusterName string
|
||||
@@ -737,6 +782,11 @@ type pack struct {
|
||||
leafWSSMessage string
|
||||
leafWSSAppURI string
|
||||
|
||||
leafTCPAppName string
|
||||
leafTCPPublicAddr string
|
||||
leafTCPMessage string
|
||||
leafTCPAppURI string
|
||||
|
||||
headerAppName string
|
||||
headerAppPublicAddr string
|
||||
headerAppClusterName string
|
||||
@@ -763,6 +813,29 @@ func setup(t *testing.T) *pack {
|
||||
return setupWithOptions(t, appTestOptions{})
|
||||
}
|
||||
|
||||
// newTCPServer starts accepting TCP connections and serving them using the
|
||||
// provided handler. Handlers are expected to close client connections.
|
||||
// Returns the TCP listener.
|
||||
func newTCPServer(t *testing.T, handleConn func(net.Conn)) net.Listener {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err == nil {
|
||||
go handleConn(conn)
|
||||
}
|
||||
if err != nil && !utils.IsOKNetworkError(err) {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return listener
|
||||
}
|
||||
|
||||
// setupWithOptions configures app access test with custom options.
|
||||
func setupWithOptions(t *testing.T, opts appTestOptions) *pack {
|
||||
tr := utils.NewTracer(utils.ThisFunction()).Start()
|
||||
@@ -788,6 +861,10 @@ func setupWithOptions(t *testing.T, opts appTestOptions) *pack {
|
||||
rootWSSPublicAddr: "wss-01.example.com",
|
||||
rootWSSMessage: uuid.New().String(),
|
||||
|
||||
rootTCPAppName: "tcp-01",
|
||||
rootTCPPublicAddr: "tcp-01.example.com",
|
||||
rootTCPMessage: uuid.New().String(),
|
||||
|
||||
leafAppName: "app-02",
|
||||
leafAppPublicAddr: "app-02.example.com",
|
||||
leafAppClusterName: "leaf.example.com",
|
||||
@@ -801,6 +878,10 @@ func setupWithOptions(t *testing.T, opts appTestOptions) *pack {
|
||||
leafWSSPublicAddr: "wss-02.example.com",
|
||||
leafWSSMessage: uuid.New().String(),
|
||||
|
||||
leafTCPAppName: "tcp-02",
|
||||
leafTCPPublicAddr: "tcp-02.example.com",
|
||||
leafTCPMessage: uuid.New().String(),
|
||||
|
||||
jwtAppName: "app-03",
|
||||
jwtAppPublicAddr: "app-03.example.com",
|
||||
jwtAppClusterName: "example.com",
|
||||
@@ -844,6 +925,13 @@ func setupWithOptions(t *testing.T, opts appTestOptions) *pack {
|
||||
conn.Close()
|
||||
}))
|
||||
t.Cleanup(rootWSSServer.Close)
|
||||
// Plain TCP application in root cluster (tcp://).
|
||||
rootTCPServer := newTCPServer(t, func(c net.Conn) {
|
||||
c.Write([]byte(p.rootTCPMessage))
|
||||
c.Close()
|
||||
})
|
||||
t.Cleanup(func() { rootTCPServer.Close() })
|
||||
// HTTP server in leaf cluster.
|
||||
leafServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, p.leafMessage)
|
||||
}))
|
||||
@@ -860,6 +948,12 @@ func setupWithOptions(t *testing.T, opts appTestOptions) *pack {
|
||||
conn.Close()
|
||||
}))
|
||||
t.Cleanup(leafWSSServer.Close)
|
||||
// Plain TCP application in leaf cluster (tcp://).
|
||||
leafTCPServer := newTCPServer(t, func(c net.Conn) {
|
||||
c.Write([]byte(p.leafTCPMessage))
|
||||
c.Close()
|
||||
})
|
||||
t.Cleanup(func() { leafTCPServer.Close() })
|
||||
jwtServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, r.Header.Get(teleport.AppJWTHeader))
|
||||
}))
|
||||
@@ -898,9 +992,11 @@ func setupWithOptions(t *testing.T, opts appTestOptions) *pack {
|
||||
p.rootAppURI = rootServer.URL
|
||||
p.rootWSAppURI = rootWSServer.URL
|
||||
p.rootWSSAppURI = rootWSSServer.URL
|
||||
p.rootTCPAppURI = fmt.Sprintf("tcp://%v", rootTCPServer.Addr().String())
|
||||
p.leafAppURI = leafServer.URL
|
||||
p.leafWSAppURI = leafWSServer.URL
|
||||
p.leafWSSAppURI = leafWSSServer.URL
|
||||
p.leafTCPAppURI = fmt.Sprintf("tcp://%v", leafTCPServer.Addr().String())
|
||||
p.jwtAppURI = jwtServer.URL
|
||||
p.headerAppURI = headerServer.URL
|
||||
p.flushAppURI = flushServer.URL
|
||||
@@ -1185,6 +1281,29 @@ func (p *pack) initCertPool(t *testing.T) {
|
||||
p.rootCertPool = pool
|
||||
}
|
||||
|
||||
// startLocalProxy starts a local ALPN proxy for the specified application.
|
||||
func (p *pack) startLocalProxy(t *testing.T, publicAddr, clusterName string) string {
|
||||
tlsConfig := p.makeTLSConfig(t, publicAddr, clusterName)
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy, err := alpnproxy.NewLocalProxy(alpnproxy.LocalProxyConfig{
|
||||
RemoteProxyAddr: p.rootCluster.Web,
|
||||
Protocols: []alpncommon.Protocol{alpncommon.ProtocolTCP},
|
||||
InsecureSkipVerify: true,
|
||||
Listener: listener,
|
||||
ParentContext: context.Background(),
|
||||
Certs: tlsConfig.Certificates,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { proxy.Close() })
|
||||
|
||||
go proxy.Start(context.Background())
|
||||
|
||||
return proxy.GetAddr()
|
||||
}
|
||||
|
||||
// makeTLSConfig returns TLS config suitable for making an app access request.
|
||||
func (p *pack) makeTLSConfig(t *testing.T, publicAddr, clusterName string) *tls.Config {
|
||||
privateKey, publicKey, err := native.GenerateKeyPair()
|
||||
@@ -1410,6 +1529,11 @@ func (p *pack) startRootAppServers(t *testing.T, count int, extraApps []service.
|
||||
URI: p.rootWSSAppURI,
|
||||
PublicAddr: p.rootWSSPublicAddr,
|
||||
},
|
||||
{
|
||||
Name: p.rootTCPAppName,
|
||||
URI: p.rootTCPAppURI,
|
||||
PublicAddr: p.rootTCPPublicAddr,
|
||||
},
|
||||
{
|
||||
Name: p.jwtAppName,
|
||||
URI: p.jwtAppURI,
|
||||
@@ -1539,6 +1663,11 @@ func (p *pack) startLeafAppServers(t *testing.T, count int, extraApps []service.
|
||||
URI: p.leafWSSAppURI,
|
||||
PublicAddr: p.leafWSSPublicAddr,
|
||||
},
|
||||
{
|
||||
Name: p.leafTCPAppName,
|
||||
URI: p.leafTCPAppURI,
|
||||
PublicAddr: p.leafTCPPublicAddr,
|
||||
},
|
||||
{
|
||||
Name: "dumper-leaf",
|
||||
URI: p.dumperAppURI,
|
||||
|
||||
@@ -384,6 +384,8 @@ const (
|
||||
|
||||
// AppSessionStartEvent is emitted when a user is issued an application certificate.
|
||||
AppSessionStartEvent = "app.session.start"
|
||||
// AppSessionEndEvent is emitted when a user connects to a TCP application.
|
||||
AppSessionEndEvent = "app.session.end"
|
||||
|
||||
// AppSessionChunkEvent is emitted at the start of a 5 minute chunk on each
|
||||
// proxy. This chunk is used to buffer 5 minutes of audit events at a time
|
||||
|
||||
@@ -106,6 +106,8 @@ const (
|
||||
|
||||
// AppSessionStartCode is the application session start code.
|
||||
AppSessionStartCode = "T2007I"
|
||||
// AppSessionEndCode is the application session end event code.
|
||||
AppSessionEndCode = "T2011I"
|
||||
// AppSessionChunkCode is the application session chunk create code.
|
||||
AppSessionChunkCode = "T2008I"
|
||||
// AppSessionRequestCode is the application request/response code.
|
||||
|
||||
@@ -145,6 +145,8 @@ func FromEventFields(fields EventFields) (events.AuditEvent, error) {
|
||||
e = &events.SessionReject{}
|
||||
case AppSessionStartEvent:
|
||||
e = &events.AppSessionStart{}
|
||||
case AppSessionEndEvent:
|
||||
e = &events.AppSessionEnd{}
|
||||
case AppSessionChunkEvent:
|
||||
e = &events.AppSessionChunk{}
|
||||
case AppSessionRequestEvent:
|
||||
|
||||
@@ -3428,6 +3428,15 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error {
|
||||
log.Info("Web UI is disabled.")
|
||||
}
|
||||
|
||||
// Register ALPN handler that will be accepting connections for plain
|
||||
// TCP applications.
|
||||
if alpnRouter != nil {
|
||||
alpnRouter.Add(alpnproxy.HandlerDecs{
|
||||
MatchFunc: alpnproxy.MatchByProtocol(alpncommon.ProtocolTCP),
|
||||
Handler: webHandler.HandleConnection,
|
||||
})
|
||||
}
|
||||
|
||||
var peerAddr string
|
||||
var proxyServer *proxy.Server
|
||||
if !process.Config.Proxy.DisableReverseTunnel && listeners.proxy != nil {
|
||||
|
||||
@@ -504,6 +504,7 @@ func TestSetupProxyTLSConfig(t *testing.T) {
|
||||
"teleport-proxy-ssh",
|
||||
"teleport-reversetunnel",
|
||||
"teleport-auth@",
|
||||
"teleport-tcp",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -522,6 +523,7 @@ func TestSetupProxyTLSConfig(t *testing.T) {
|
||||
"teleport-proxy-ssh",
|
||||
"teleport-reversetunnel",
|
||||
"teleport-auth@",
|
||||
"teleport-tcp",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -76,6 +76,9 @@ const (
|
||||
// ProtocolMySQLWithVerPrefix is TLS ALPN prefix used by tsh to carry
|
||||
// MySQL server version.
|
||||
ProtocolMySQLWithVerPrefix = Protocol(string(ProtocolMySQL) + "-")
|
||||
|
||||
// ProtocolTCP is TLS ALPN protocol value used to indicate plain TCP connection.
|
||||
ProtocolTCP Protocol = "teleport-tcp"
|
||||
)
|
||||
|
||||
// SupportedProtocols is the list of supported ALPN protocols.
|
||||
@@ -91,6 +94,7 @@ var SupportedProtocols = []Protocol{
|
||||
ProtocolProxySSH,
|
||||
ProtocolReverseTunnel,
|
||||
ProtocolAuth,
|
||||
ProtocolTCP,
|
||||
}
|
||||
|
||||
// ProtocolsToString converts the list of Protocols to the list of strings.
|
||||
|
||||
+97
-19
@@ -170,6 +170,7 @@ type Server struct {
|
||||
closeFunc context.CancelFunc
|
||||
|
||||
httpServer *http.Server
|
||||
tcpServer *tcpServer
|
||||
tlsConfig *tls.Config
|
||||
|
||||
mu sync.RWMutex
|
||||
@@ -193,6 +194,9 @@ type Server struct {
|
||||
|
||||
// watcher monitors changes to application resources.
|
||||
watcher *services.AppWatcher
|
||||
|
||||
// authMiddleware allows wrapping connections with identity information.
|
||||
authMiddleware *auth.Middleware
|
||||
}
|
||||
|
||||
// monitoredApps is a collection of applications from different sources
|
||||
@@ -259,9 +263,9 @@ func New(ctx context.Context, c *Config) (*Server, error) {
|
||||
|
||||
// Create and configure HTTP server with authorizing middleware.
|
||||
s.httpServer = s.newHTTPServer()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// TCP server will handle TCP applications.
|
||||
s.tcpServer = s.newTCPServer()
|
||||
|
||||
// Create a new session cache, this holds sessions that can be used to
|
||||
// forward requests.
|
||||
@@ -576,9 +580,52 @@ func (s *Server) HandleConnection(conn net.Conn) {
|
||||
// httpServer will initiate the close call.
|
||||
closerConn := utils.NewCloserConn(conn)
|
||||
|
||||
if err := s.handleConnection(closerConn); err != nil {
|
||||
s.log.WithError(err).Warnf("Failed to handle client connection.")
|
||||
if err := conn.Close(); err != nil {
|
||||
s.log.WithError(err).Warnf("Failed to close client connection.")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for connection to close.
|
||||
closerConn.Wait()
|
||||
}
|
||||
|
||||
func (s *Server) handleConnection(conn net.Conn) error {
|
||||
// Proxy sends a X.509 client certificate to pass identity information,
|
||||
// extract it and run authorization checks on it.
|
||||
tlsConn, user, app, err := s.getConnectionInfo(s.closeContext, conn)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Application access supports plain TCP connections which are handled
|
||||
// differently than HTTP requests from web apps.
|
||||
if app.IsTCP() {
|
||||
return s.handleTCPApp(s.closeContext, tlsConn, user, app)
|
||||
}
|
||||
|
||||
return s.handleHTTPApp(s.closeContext, tlsConn)
|
||||
}
|
||||
|
||||
// handleTCPApp handles connection for a TCP application.
|
||||
func (s *Server) handleTCPApp(ctx context.Context, conn net.Conn, user auth.IdentityGetter, app types.Application) error {
|
||||
id, _, err := s.authorizeContext(context.WithValue(ctx, auth.ContextUser, user))
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
err = s.tcpServer.handleConnection(s.closeContext, conn, id, app)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleHTTPApp handles connection for an HTTP application.
|
||||
func (s *Server) handleHTTPApp(ctx context.Context, conn net.Conn) error {
|
||||
// Wrap a TLS authorizing conn in a single-use listener.
|
||||
tlsConn := tls.Server(closerConn, s.tlsConfig)
|
||||
listener := newListener(s.closeContext, tlsConn)
|
||||
listener := newListener(s.closeContext, conn)
|
||||
|
||||
// Serve will return as soon as tlsConn is running in its own goroutine
|
||||
err := s.httpServer.Serve(listener)
|
||||
@@ -586,12 +633,10 @@ func (s *Server) HandleConnection(conn net.Conn) {
|
||||
// okay to ignore errListenerConnServed; it is a signal that our
|
||||
// single-use listener has passed the connection to http.Serve
|
||||
// and conn is being served. See listener.Accept for details.
|
||||
s.log.Warnf("Failed to handle connection: %v.", err)
|
||||
return
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Wait for connection to close.
|
||||
closerConn.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeHTTP will forward the *http.Request to the target application.
|
||||
@@ -608,7 +653,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
// Extract the identity and application being requested from the certificate
|
||||
// and check if the caller has access.
|
||||
identity, app, err := s.authorize(r.Context(), r)
|
||||
identity, app, err := s.authorizeContext(r.Context())
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
@@ -661,11 +706,35 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// authorize will check if request carries a session cookie matching a
|
||||
// session in the backend.
|
||||
func (s *Server) authorize(ctx context.Context, r *http.Request) (*tlsca.Identity, types.Application, error) {
|
||||
// getConnectionInfo extracts identity information from the provided
|
||||
// connection and runs authorization checks on it.
|
||||
//
|
||||
// The connection comes from the reverse tunnel and is expected to be TLS and
|
||||
// carry identity in the client certificate.
|
||||
func (s *Server) getConnectionInfo(ctx context.Context, conn net.Conn) (*tls.Conn, auth.IdentityGetter, types.Application, error) {
|
||||
tlsConn := tls.Server(conn, s.tlsConfig)
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
return nil, nil, nil, trace.Wrap(err, "TLS handshake failed")
|
||||
}
|
||||
|
||||
user, err := s.authMiddleware.GetUser(tlsConn.ConnectionState())
|
||||
if err != nil {
|
||||
return nil, nil, nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
app, err := s.getApp(ctx, user.GetIdentity().RouteToApp.PublicAddr)
|
||||
if err != nil {
|
||||
return nil, nil, nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return tlsConn, user, app, nil
|
||||
}
|
||||
|
||||
// authorizeContext will check if the context carries identity information and
|
||||
// runs authorization checks on it.
|
||||
func (s *Server) authorizeContext(ctx context.Context) (*tlsca.Identity, types.Application, error) {
|
||||
// Only allow local and remote identities to proxy to an application.
|
||||
userType := r.Context().Value(auth.ContextUser)
|
||||
userType := ctx.Value(auth.ContextUser)
|
||||
switch userType.(type) {
|
||||
case auth.LocalUser, auth.RemoteUser:
|
||||
default:
|
||||
@@ -673,14 +742,14 @@ func (s *Server) authorize(ctx context.Context, r *http.Request) (*tlsca.Identit
|
||||
}
|
||||
|
||||
// Extract authorizing context and identity of the user from the request.
|
||||
authContext, err := s.c.Authorizer.Authorize(r.Context())
|
||||
authContext, err := s.c.Authorizer.Authorize(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
identity := authContext.Identity.GetIdentity()
|
||||
|
||||
// Fetch the application and check if the identity has access.
|
||||
app, err := s.getApp(r.Context(), identity.RouteToApp.PublicAddr)
|
||||
app, err := s.getApp(ctx, identity.RouteToApp.PublicAddr)
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
@@ -756,19 +825,28 @@ func (s *Server) getApp(ctx context.Context, publicAddr string) (types.Applicati
|
||||
func (s *Server) newHTTPServer() *http.Server {
|
||||
// Reuse the auth.Middleware to authorize requests but only accept
|
||||
// certificates that were specifically generated for applications.
|
||||
authMiddleware := &auth.Middleware{
|
||||
s.authMiddleware = &auth.Middleware{
|
||||
AccessPoint: s.c.AccessPoint,
|
||||
AcceptedUsage: []string{teleport.UsageAppsOnly},
|
||||
}
|
||||
authMiddleware.Wrap(s)
|
||||
s.authMiddleware.Wrap(s)
|
||||
|
||||
return &http.Server{
|
||||
Handler: authMiddleware,
|
||||
Handler: s.authMiddleware,
|
||||
ReadHeaderTimeout: apidefaults.DefaultDialTimeout,
|
||||
ErrorLog: utils.NewStdlogger(s.log.Error, teleport.ComponentApp),
|
||||
}
|
||||
}
|
||||
|
||||
// newTCPServer creates a server that proxies TCP applications.
|
||||
func (s *Server) newTCPServer() *tcpServer {
|
||||
return &tcpServer{
|
||||
authClient: s.c.AuthClient,
|
||||
hostID: s.c.HostID,
|
||||
log: s.log,
|
||||
}
|
||||
}
|
||||
|
||||
// getProxyPort tries to figure out the address the proxy is running at.
|
||||
func (s *Server) getProxyPort() string {
|
||||
servers, err := s.c.AccessPoint.GetProxies()
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
Copyright 2022 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
apidefaults "github.com/gravitational/teleport/api/defaults"
|
||||
apitypes "github.com/gravitational/teleport/api/types"
|
||||
apievents "github.com/gravitational/teleport/api/types/events"
|
||||
"github.com/gravitational/teleport/lib/auth"
|
||||
"github.com/gravitational/teleport/lib/events"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type tcpServer struct {
|
||||
authClient *auth.Client
|
||||
hostID string
|
||||
log logrus.FieldLogger
|
||||
}
|
||||
|
||||
// handleConnection handles connection from a TCP application.
|
||||
func (s *tcpServer) handleConnection(ctx context.Context, clientConn net.Conn, identity *tlsca.Identity, app apitypes.Application) error {
|
||||
addr, err := utils.ParseAddr(app.GetURI())
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
if addr.AddrNetwork != "tcp" {
|
||||
return trace.BadParameter(`unexpected app %q address network, expected "tcp": %+v`, app.GetName(), addr)
|
||||
}
|
||||
dialer := net.Dialer{
|
||||
Timeout: apidefaults.DefaultDialTimeout,
|
||||
}
|
||||
serverConn, err := dialer.DialContext(ctx, addr.AddrNetwork, addr.String())
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
err = s.emitStartEvent(ctx, identity, app)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
defer func() {
|
||||
err = s.emitEndEvent(ctx, identity, app)
|
||||
if err != nil {
|
||||
s.log.WithError(err).Warnf("Failed to emit session end event for app %v.", app.GetName())
|
||||
}
|
||||
}()
|
||||
err = utils.ProxyConn(ctx, clientConn, serverConn)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tcpServer) emitStartEvent(ctx context.Context, identity *tlsca.Identity, app apitypes.Application) error {
|
||||
return s.authClient.EmitAuditEvent(ctx, &apievents.AppSessionStart{
|
||||
Metadata: apievents.Metadata{
|
||||
Type: events.AppSessionStartEvent,
|
||||
Code: events.AppSessionStartCode,
|
||||
ClusterName: identity.RouteToApp.ClusterName,
|
||||
},
|
||||
ServerMetadata: apievents.ServerMetadata{
|
||||
ServerID: s.hostID,
|
||||
ServerNamespace: apidefaults.Namespace,
|
||||
},
|
||||
SessionMetadata: apievents.SessionMetadata{
|
||||
SessionID: identity.RouteToApp.SessionID,
|
||||
WithMFA: identity.MFAVerified,
|
||||
},
|
||||
UserMetadata: identity.GetUserMetadata(),
|
||||
ConnectionMetadata: apievents.ConnectionMetadata{
|
||||
RemoteAddr: identity.ClientIP,
|
||||
},
|
||||
AppMetadata: apievents.AppMetadata{
|
||||
AppURI: app.GetURI(),
|
||||
AppPublicAddr: app.GetPublicAddr(),
|
||||
AppName: app.GetName(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *tcpServer) emitEndEvent(ctx context.Context, identity *tlsca.Identity, app apitypes.Application) error {
|
||||
return s.authClient.EmitAuditEvent(ctx, &apievents.AppSessionEnd{
|
||||
Metadata: apievents.Metadata{
|
||||
Type: events.AppSessionEndEvent,
|
||||
Code: events.AppSessionEndCode,
|
||||
ClusterName: identity.RouteToApp.ClusterName,
|
||||
},
|
||||
ServerMetadata: apievents.ServerMetadata{
|
||||
ServerID: s.hostID,
|
||||
ServerNamespace: apidefaults.Namespace,
|
||||
},
|
||||
SessionMetadata: apievents.SessionMetadata{
|
||||
SessionID: identity.RouteToApp.SessionID,
|
||||
WithMFA: identity.MFAVerified,
|
||||
},
|
||||
UserMetadata: identity.GetUserMetadata(),
|
||||
ConnectionMetadata: apievents.ConnectionMetadata{
|
||||
RemoteAddr: identity.ClientIP,
|
||||
},
|
||||
AppMetadata: apievents.AppMetadata{
|
||||
AppURI: app.GetURI(),
|
||||
AppPublicAddr: app.GetPublicAddr(),
|
||||
AppName: app.GetName(),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright 2022 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
)
|
||||
|
||||
// ProxyConn launches a double-copy loop that proxies traffic between the
|
||||
// provided client and server connections.
|
||||
//
|
||||
// Exits when one or both copies stop, or when the context is canceled, and
|
||||
// closes both connections.
|
||||
func ProxyConn(ctx context.Context, client, server io.ReadWriteCloser) error {
|
||||
errCh := make(chan error, 2)
|
||||
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
|
||||
go func() {
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
_, err := io.Copy(server, client)
|
||||
errCh <- err
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
_, err := io.Copy(client, server)
|
||||
errCh <- err
|
||||
}()
|
||||
|
||||
var errors []error
|
||||
for i := 0; i < 2; i++ {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil && !IsOKNetworkError(err) {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return trace.NewAggregate(errors...)
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
Copyright 2022 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestProxyConn tests proxying the connection between client and server.
|
||||
func TestProxyConn(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
echoServer, err := newEchoServer()
|
||||
require.NoError(t, err)
|
||||
go echoServer.Start()
|
||||
t.Cleanup(func() { echoServer.Close() })
|
||||
|
||||
echoConn, err := net.Dial("tcp", echoServer.Addr())
|
||||
require.NoError(t, err)
|
||||
// Connection will be closed below.
|
||||
|
||||
// Simulate the client connection with pipe.
|
||||
clientLeft, clientRight := net.Pipe()
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(errCh)
|
||||
err := ProxyConn(ctx, clientRight, echoConn)
|
||||
if err != nil && !strings.Contains(err.Error(), io.ErrClosedPipe.Error()) {
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
|
||||
// Send message to the echo server through the proxy and expect to get
|
||||
// the same one back.
|
||||
sent := uuid.NewString()
|
||||
_, err = clientLeft.Write([]byte(sent))
|
||||
require.NoError(t, err)
|
||||
|
||||
received := make([]byte, 36)
|
||||
_, err = clientLeft.Read(received)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, sent, string(received))
|
||||
fmt.Println(string(received))
|
||||
|
||||
// Close the server connection and make sure the proxy loop exits.
|
||||
echoConn.Close()
|
||||
select {
|
||||
case err := <-errCh:
|
||||
require.NoError(t, err)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("proxy loop didn't exit after 1s")
|
||||
}
|
||||
}
|
||||
|
||||
// TestProxyConnCancel verifies context cancellation for the proxy loop.
|
||||
func TestProxyConnCancel(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
echoServer, err := newEchoServer()
|
||||
require.NoError(t, err)
|
||||
go echoServer.Start()
|
||||
t.Cleanup(func() { echoServer.Close() })
|
||||
|
||||
echoConn, err := net.Dial("tcp", echoServer.Addr())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { echoConn.Close() })
|
||||
|
||||
_, clientRight := net.Pipe()
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(errCh)
|
||||
errCh <- ProxyConn(ctx, clientRight, echoConn)
|
||||
}()
|
||||
|
||||
// Cancel the context and make sure the proxy loop exits.
|
||||
cancel()
|
||||
select {
|
||||
case err := <-errCh:
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("proxy loop didn't exit after 1s")
|
||||
}
|
||||
}
|
||||
|
||||
type echoServer struct {
|
||||
listener net.Listener
|
||||
log logrus.FieldLogger
|
||||
}
|
||||
|
||||
func newEchoServer() (*echoServer, error) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return &echoServer{
|
||||
listener: listener,
|
||||
log: logrus.WithField(trace.Component, "echo"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *echoServer) Addr() string {
|
||||
return s.listener.Addr().String()
|
||||
}
|
||||
|
||||
func (s *echoServer) Close() error {
|
||||
return s.listener.Close()
|
||||
}
|
||||
|
||||
func (s *echoServer) Start() error {
|
||||
for {
|
||||
conn, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
go s.handleConn(conn)
|
||||
// Don't close the connection to let the proxy handle it.
|
||||
}
|
||||
}
|
||||
|
||||
func (s *echoServer) handleConn(conn net.Conn) error {
|
||||
b := make([]byte, 36) // expect to receive UUID from the test
|
||||
_, err := conn.Read(b)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
s.log.Infof("Received message: %s.", b)
|
||||
|
||||
_, err = conn.Write(b)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -218,6 +218,11 @@ func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// HandleConnection handles connections from plain TCP applications.
|
||||
func (h *APIHandler) HandleConnection(ctx context.Context, conn net.Conn) error {
|
||||
return h.appHandler.HandleConnection(ctx, conn)
|
||||
}
|
||||
|
||||
func (h *APIHandler) Close() error {
|
||||
return h.handler.Close()
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -136,6 +137,51 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.router.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// HandleConnection handles connections from plain TCP applications.
|
||||
func (h *Handler) HandleConnection(ctx context.Context, clientConn net.Conn) error {
|
||||
tlsConn, ok := clientConn.(*tls.Conn)
|
||||
if !ok {
|
||||
return trace.BadParameter("expected *tls.Conn, got: %T", clientConn)
|
||||
}
|
||||
|
||||
certs := tlsConn.ConnectionState().PeerCertificates
|
||||
if len(certs) != 1 {
|
||||
return trace.BadParameter("expected 1 client certificate: %+v", tlsConn.ConnectionState())
|
||||
}
|
||||
|
||||
identity, err := tlsca.FromSubject(certs[0].Subject, certs[0].NotAfter)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
ws, err := h.c.AccessPoint.GetAppSession(ctx, types.GetAppSessionRequest{
|
||||
SessionID: identity.RouteToApp.SessionID,
|
||||
})
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
session, err := h.getSession(ctx, ws)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
serverConn, err := session.tr.DialContext(ctx, "", "")
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
defer serverConn.Close()
|
||||
|
||||
serverConn = tls.Client(serverConn, session.tr.clientTLSConfig)
|
||||
|
||||
err = utils.ProxyConn(ctx, clientConn, serverConn)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleForward forwards the request to the application service.
|
||||
func (h *Handler) handleForward(w http.ResponseWriter, r *http.Request, session *session) error {
|
||||
session.fwd.ServeHTTP(w, r)
|
||||
|
||||
@@ -39,6 +39,8 @@ type session struct {
|
||||
fwd *forward.Forwarder
|
||||
// ws represents the services.WebSession this requests belongs to.
|
||||
ws types.WebSession
|
||||
// transport allows to dial an application server.
|
||||
tr *transport
|
||||
}
|
||||
|
||||
// newSession creates a new session.
|
||||
@@ -113,6 +115,7 @@ func (h *Handler) newSession(ctx context.Context, ws types.WebSession) (*session
|
||||
return &session{
|
||||
fwd: fwd,
|
||||
ws: ws,
|
||||
tr: transport,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -65,7 +65,10 @@ func (h *Handler) clusterAppsGet(w http.ResponseWriter, r *http.Request, p httpr
|
||||
|
||||
var apps types.Apps
|
||||
for _, server := range appServers {
|
||||
apps = append(apps, server.GetApp())
|
||||
// Skip over TCP apps since they cannot be accessed through web UI.
|
||||
if !server.GetApp().IsTCP() {
|
||||
apps = append(apps, server.GetApp())
|
||||
}
|
||||
}
|
||||
|
||||
return listResourcesGetResponse{
|
||||
|
||||
@@ -490,10 +490,10 @@ func (a *appServerCollection) writeText(w io.Writer) error {
|
||||
app := server.GetApp()
|
||||
labels := stripInternalTeleportLabels(a.verbose, app.GetAllLabels())
|
||||
rows = append(rows, []string{
|
||||
server.GetHostname(), app.GetName(), app.GetPublicAddr(), app.GetURI(), labels, server.GetTeleportVersion()})
|
||||
server.GetHostname(), app.GetName(), app.GetProtocol(), app.GetPublicAddr(), app.GetURI(), labels, server.GetTeleportVersion()})
|
||||
}
|
||||
var t asciitable.Table
|
||||
headers := []string{"Host", "Name", "Public Address", "URI", "Labels", "Version"}
|
||||
headers := []string{"Host", "Name", "Type", "Public Address", "URI", "Labels", "Version"}
|
||||
if a.verbose {
|
||||
t = asciitable.MakeTable(headers, rows...)
|
||||
} else {
|
||||
|
||||
+22
-5
@@ -100,6 +100,11 @@ func onAppLogin(cf *CLIConf) error {
|
||||
"awsCmd": "s3 ls",
|
||||
})
|
||||
}
|
||||
if app.IsTCP() {
|
||||
return appLoginTCPTpl.Execute(os.Stdout, map[string]string{
|
||||
"appName": app.GetName(),
|
||||
})
|
||||
}
|
||||
curlCmd, err := formatAppConfig(tc, profile, app.GetName(), app.GetPublicAddr(), appFormatCURL, rootCluster)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
@@ -110,18 +115,30 @@ func onAppLogin(cf *CLIConf) error {
|
||||
})
|
||||
}
|
||||
|
||||
// appLoginTpl is the message that gets printed to a user upon successful app login.
|
||||
// appLoginTpl is the message that gets printed to a user upon successful login
|
||||
// into an HTTP application.
|
||||
var appLoginTpl = template.Must(template.New("").Parse(
|
||||
`Logged into app {{.appName}}. Example curl command:
|
||||
|
||||
{{.curlCmd}}
|
||||
`))
|
||||
|
||||
// awsCliTpl is the message that gets printed to a user upon successful aws app login.
|
||||
var awsCliTpl = template.Must(template.New("").Parse(
|
||||
`Logged into AWS app {{.awsAppName}}. Example AWS cli command:
|
||||
// appLoginTCPTpl is the message that gets printed to a user upon successful
|
||||
// login into a TCP application.
|
||||
var appLoginTCPTpl = template.Must(template.New("").Parse(
|
||||
`Logged into TCP app {{.appName}}. Start the local TCP proxy for it:
|
||||
|
||||
tsh aws {{.awsCmd}}
|
||||
tsh proxy app {{.appName}}
|
||||
|
||||
Then connect to the application through this proxy.
|
||||
`))
|
||||
|
||||
// awsCliTpl is the message that gets printed to a user upon successful login
|
||||
// into an AWS Console application.
|
||||
var awsCliTpl = template.Must(template.New("").Parse(
|
||||
`Logged into AWS app {{.awsAppName}}. Example AWS CLI command:
|
||||
|
||||
tsh aws {{.awsCmd}}
|
||||
`))
|
||||
|
||||
// getRegisteredApp returns the registered application with the specified name.
|
||||
|
||||
+14
-1
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/gravitational/teleport/api/client/webclient"
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
tracessh "github.com/gravitational/teleport/api/observability/tracing/ssh"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
libclient "github.com/gravitational/teleport/lib/client"
|
||||
"github.com/gravitational/teleport/lib/client/db/dbcmd"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
@@ -477,6 +478,13 @@ func mkLocalProxyCerts(certFile, keyFile string) ([]tls.Certificate, error) {
|
||||
return []tls.Certificate{cert}, nil
|
||||
}
|
||||
|
||||
func alpnProtocolForApp(app types.Application) alpncommon.Protocol {
|
||||
if app.IsTCP() {
|
||||
return alpncommon.ProtocolTCP
|
||||
}
|
||||
return alpncommon.ProtocolHTTP
|
||||
}
|
||||
|
||||
func onProxyCommandApp(cf *CLIConf) error {
|
||||
tc, err := makeClient(cf, false)
|
||||
if err != nil {
|
||||
@@ -488,6 +496,11 @@ func onProxyCommandApp(cf *CLIConf) error {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
app, err := getRegisteredApp(cf, tc)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
address, err := utils.ParseAddr(tc.WebProxyAddr)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
@@ -506,7 +519,7 @@ func onProxyCommandApp(cf *CLIConf) error {
|
||||
lp, err := alpnproxy.NewLocalProxy(alpnproxy.LocalProxyConfig{
|
||||
Listener: listener,
|
||||
RemoteProxyAddr: tc.WebProxyAddr,
|
||||
Protocols: []alpncommon.Protocol{alpncommon.ProtocolHTTP},
|
||||
Protocols: []alpncommon.Protocol{alpnProtocolForApp(app)},
|
||||
InsecureSkipVerify: cf.InsecureSkipVerify,
|
||||
ParentContext: cf.Context,
|
||||
SNI: address.Host(),
|
||||
|
||||
+8
-6
@@ -2060,11 +2060,13 @@ func getAppRow(proxy, cluster string, app types.Application, active []tlsca.Rout
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if verbose {
|
||||
row = append(row, name, app.GetDescription(), app.GetPublicAddr(), app.GetURI(), sortedLabels(app.GetAllLabels()))
|
||||
row = append(row, name, app.GetDescription(), app.GetProtocol(), app.GetPublicAddr(), app.GetURI(), sortedLabels(app.GetAllLabels()))
|
||||
} else {
|
||||
row = append(row, name, app.GetDescription(), app.GetPublicAddr(), sortedLabels(app.GetAllLabels()))
|
||||
row = append(row, name, app.GetDescription(), app.GetProtocol(), app.GetPublicAddr(), sortedLabels(app.GetAllLabels()))
|
||||
}
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
@@ -2078,10 +2080,10 @@ func showAppsAsText(apps []types.Application, active []tlsca.RouteToApp, verbose
|
||||
// lines per node.
|
||||
var t asciitable.Table
|
||||
if verbose {
|
||||
t = asciitable.MakeTable([]string{"Application", "Description", "Public Address", "URI", "Labels"}, rows...)
|
||||
t = asciitable.MakeTable([]string{"Application", "Description", "Type", "Public Address", "URI", "Labels"}, rows...)
|
||||
} else {
|
||||
t = asciitable.MakeTableWithTruncatedColumn(
|
||||
[]string{"Application", "Description", "Public Address", "Labels"}, rows, "Labels")
|
||||
[]string{"Application", "Description", "Type", "Public Address", "Labels"}, rows, "Labels")
|
||||
}
|
||||
fmt.Println(t.AsBuffer().String())
|
||||
}
|
||||
@@ -3775,10 +3777,10 @@ func printAppsWithClusters(apps []appListing, active []tlsca.RouteToApp, verbose
|
||||
// lines per node.
|
||||
var t asciitable.Table
|
||||
if verbose {
|
||||
t = asciitable.MakeTable([]string{"Proxy", "Cluster", "Application", "Description", "Public Address", "URI", "Labels"}, rows...)
|
||||
t = asciitable.MakeTable([]string{"Proxy", "Cluster", "Application", "Description", "Type", "Public Address", "URI", "Labels"}, rows...)
|
||||
} else {
|
||||
t = asciitable.MakeTableWithTruncatedColumn(
|
||||
[]string{"Proxy", "Cluster", "Application", "Description", "Public Address", "Labels"}, rows, "Labels")
|
||||
[]string{"Proxy", "Cluster", "Application", "Description", "Type", "Public Address", "Labels"}, rows, "Labels")
|
||||
}
|
||||
fmt.Println(t.AsBuffer().String())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user