Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions api/types/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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]
Expand Down
1,808 changes: 1,153 additions & 655 deletions api/types/events/events.pb.go

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions api/types/events/events.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1303,6 +1303,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.
Expand Down Expand Up @@ -1928,6 +1955,7 @@ message OneOf {
events.AccessRequestResourceSearch AccessRequestResourceSearch = 88;
events.SQLServerRPCRequest SQLServerRPCRequest = 89;
events.DatabaseSessionMalformedPacket DatabaseSessionMalformedPacket = 90;
events.AppSessionEnd AppSessionEnd = 93;
}
}

Expand Down
4 changes: 4 additions & 0 deletions api/types/events/oneof.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
136 changes: 132 additions & 4 deletions integration/app_integration_test.go
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -34,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"
Expand All @@ -53,10 +50,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"
Expand All @@ -83,6 +84,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)
Expand Down Expand Up @@ -175,6 +177,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 {
Expand Down Expand Up @@ -711,6 +750,11 @@ type pack struct {
rootWSSMessage string
rootWSSAppURI string

rootTCPAppName string
rootTCPPublicAddr string
rootTCPMessage string
rootTCPAppURI string

jwtAppName string
jwtAppPublicAddr string
jwtAppClusterName string
Expand Down Expand Up @@ -738,6 +782,11 @@ type pack struct {
leafWSSMessage string
leafWSSAppURI string

leafTCPAppName string
leafTCPPublicAddr string
leafTCPMessage string
leafTCPAppURI string

headerAppName string
headerAppPublicAddr string
headerAppClusterName string
Expand All @@ -764,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()
Expand All @@ -789,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",
Expand All @@ -802,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",
Expand Down Expand Up @@ -845,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)
}))
Expand All @@ -861,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))
}))
Expand Down Expand Up @@ -899,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
Expand Down Expand Up @@ -1180,6 +1275,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.GetWebAddr(),
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()
Expand Down Expand Up @@ -1405,6 +1523,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,
Expand Down Expand Up @@ -1534,6 +1657,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,
Expand Down
2 changes: 2 additions & 0 deletions lib/events/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,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
Expand Down
2 changes: 2 additions & 0 deletions lib/events/codes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions lib/events/dynamic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions lib/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3426,6 +3426,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 {
Expand Down
2 changes: 2 additions & 0 deletions lib/service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,7 @@ func TestSetupProxyTLSConfig(t *testing.T) {
"teleport-proxy-ssh",
"teleport-reversetunnel",
"teleport-auth@",
"teleport-tcp",
},
},
{
Expand All @@ -522,6 +523,7 @@ func TestSetupProxyTLSConfig(t *testing.T) {
"teleport-proxy-ssh",
"teleport-reversetunnel",
"teleport-auth@",
"teleport-tcp",
},
},
}
Expand Down
Loading