Add support for proxying TCP apps (#13455)

Add support for proxying tcp apps
This commit is contained in:
Roman Tkachenko
2022-07-26 19:01:39 +00:00
committed by GitHub
parent 6fe1c8e27e
commit 38b8bb4307
23 changed files with 1843 additions and 628 deletions
+17
View File
@@ -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]
File diff suppressed because it is too large Load Diff
+28
View File
@@ -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;
}
}
+4
View File
@@ -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,
+133 -4
View File
@@ -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,
+2
View File
@@ -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
+2
View File
@@ -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.
+2
View File
@@ -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:
+9
View File
@@ -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 {
+2
View File
@@ -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",
},
},
}
+4
View File
@@ -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
View File
@@ -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()
+126
View File
@@ -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(),
},
})
}
+64
View File
@@ -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...)
}
+161
View File
@@ -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
}
+5
View File
@@ -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()
}
+46
View File
@@ -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)
+3
View File
@@ -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
View File
@@ -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{
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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())
}