diff --git a/lib/web/app/transport.go b/lib/web/app/transport.go index d186aa805e438..44fbe80f1a442 100644 --- a/lib/web/app/transport.go +++ b/lib/web/app/transport.go @@ -361,7 +361,22 @@ func (t *transport) DialContext(ctx context.Context, _, _ string) (conn net.Conn appIntegration := appServer.GetApp().GetIntegration() if appIntegration != "" { src, dst := net.Pipe() - go t.c.integrationAppHandler.HandleConnection(src) + + // Creating the connection using `net.Pipe()` results in both ends having the same local/remote address: "pipe". + // This causes IP Pinning checks to fail when validating that the connection remote address matches the certificate pinned IP. + // + // To fix this, we need to wrap the `src` connection to return the client source address as the remote address. + // The client source address is extracted from the context, the same way as in `dialAppServer`. + srcWithClientSrcAddr, err := wrapConnWithClientSrcAddrFromContext(ctx, src) + if err != nil { + // Log a warning and use the original remote address if we fail to extract the client source address from context. + // This will result in an error if the flow has pinned IP restrictions. + t.c.log.WithFields(logrus.Fields{"app_server": appServer.GetName()}). + Warnf("Failed to extract client source address from context when proxying access to Application with integration credentials. IP Pinning checks will fail. error: %v", err) + srcWithClientSrcAddr = src + } + + go t.c.integrationAppHandler.HandleConnection(srcWithClientSrcAddr) return dst, nil } @@ -387,6 +402,27 @@ func (t *transport) DialContext(ctx context.Context, _, _ string) (conn net.Conn return nil, trace.ConnectionProblem(nil, "no application servers remaining to connect") } +type connWithCustomRemoteAddr struct { + net.Conn + remoteAddr net.Addr +} + +func (w *connWithCustomRemoteAddr) RemoteAddr() net.Addr { + return w.remoteAddr +} + +func wrapConnWithClientSrcAddrFromContext(ctx context.Context, conn net.Conn) (net.Conn, error) { + clientSrcAddr, err := authz.ClientSrcAddrFromContext(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + return &connWithCustomRemoteAddr{ + Conn: conn, + remoteAddr: clientSrcAddr, + }, nil +} + // DialWebsocket dials a websocket connection over the transport's reverse // tunnel. func (t *transport) DialWebsocket(network, address string) (net.Conn, error) { diff --git a/lib/web/app/transport_test.go b/lib/web/app/transport_test.go index 78900d6c7bbce..291dcae5598ef 100644 --- a/lib/web/app/transport_test.go +++ b/lib/web/app/transport_test.go @@ -42,6 +42,7 @@ import ( "github.com/gravitational/teleport/api/types" apiutils "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/lib/authz" "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/jwt" @@ -493,13 +494,20 @@ func Test_transport_with_integration(t *testing.T) { }) require.NoError(t, err) - conn, err := tr.DialContext(context.Background(), "", "") + ctxWithClientSrcAddr := authz.ContextWithClientSrcAddr(t.Context(), &utils.NetAddr{ + AddrNetwork: "tcp", + Addr: net.JoinHostPort("127.0.0.1", "55555"), + }) + + conn, err := tr.DialContext(ctxWithClientSrcAddr, "", "") require.NoError(t, err) require.Eventually(t, func() bool { return integrationAppHandler.getConnection() != nil }, 100*time.Millisecond, 10*time.Millisecond) + require.Equal(t, "127.0.0.1:55555", integrationAppHandler.getConnection().RemoteAddr().String()) + message := "hello world" messageSize := len(message)